GEN3: the seat generator reads acts, subject, keep and callers and writes each event's subject and stream

An event whose subject carries parts is published by a function taking each part and heard by a filter with a * per
part; an event naming keep gets the stream SEAT_<seat>_<event>, in Go and in the TypeScript contract. The reader of the
sdk's reader release reads acts (every, a language, kind:image), keep as an object and callers as a list.

Builds: design 47 G §89, §203, §238, §260, §324, §331, §396, §397

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
This commit is contained in:
2026-11-11 10:40:42 +01:00
parent fe8c41928d
commit c62650e4ff
4 changed files with 143 additions and 1 deletions
@@ -180,6 +180,13 @@ func goWhole(m *Module, d seatdef.Definition) ([]byte, []byte, error) {
}
for _, e := range d.Events {
subj := seatdef.EventSubject(d.Seat, e.Name)
if e.Keep != nil {
fmt.Fprintf(&b, "// Stream%s is the stream %s is kept in.\nconst Stream%s = %q\n\n", ident(e.Name), e.Name, ident(e.Name), keptStream(d.Seat, e.Name))
}
if parts := e.Parts(); len(parts) > 0 {
goPartsSubject(&b, d.Seat, e, parts)
continue
}
if e.ByCaller {
fmt.Fprintf(&b, "// Subject%s is where %s is published for one caller.\nfunc Subject%s(caller string) string { return %q + caller }\n\n",
ident(e.Name), e.Name, ident(e.Name), subj+".")
@@ -424,6 +431,9 @@ func fixtureTarget(d seatdef.Definition, kind, name string) (string, string, err
}
for _, msg := range ms {
if msg.Name == name {
if kind == "event" && len(msg.Parts()) > 0 {
return ident(name) + suffix, partsPattern(d.Seat, msg), nil
}
p := "^" + regexp(subj)
if msg.ByCaller {
p += `\.[A-Za-z0-9_-]+`
@@ -460,3 +470,61 @@ func regexp(s string) string {
}
return b.String()
}
// keptStream is the bus's name of the stream an event naming `keep` is kept in: `SEAT_<seat>_<event>`, which the bus
// takes (no dot) and the grants part makes (design 47 G §260, §396).
func keptStream(seat, event string) string { return "SEAT_" + seat + "_" + event }
// subjectExpr is the Go or JavaScript string expression of an event's subject with its parts as arguments.
func subjectExpr(seat string, e seatdef.Message) []string {
var expr []string
lit := "mesh.seat." + seat + ".event."
for i, tok := range strings.Split(e.SubjectPattern(), ".") {
if i > 0 {
lit += "."
}
if strings.HasPrefix(tok, "<") {
expr = append(expr, fmt.Sprintf("%q", lit), goParam(tok[1:len(tok)-1]))
lit = ""
continue
}
lit += tok
}
if lit != "" {
expr = append(expr, fmt.Sprintf("%q", lit))
}
return expr
}
// goPartsSubject writes the subject of an event whose subject carries parts (design 47 G §260, §397): a function taking
// each part in order, and the filter that hears every instance of it.
func goPartsSubject(b *bytes.Buffer, seat string, e seatdef.Message, parts []string) {
args := make([]string, len(parts))
for i, p := range parts {
args[i] = goParam(p)
}
fmt.Fprintf(b, "// Subject%s is where %s is published, on %s.\nfunc Subject%s(%s string) string { return %s }\n\n",
ident(e.Name), e.Name, "mesh.seat."+seat+".event."+e.SubjectPattern(), ident(e.Name), strings.Join(args, ", "),
strings.Join(subjectExpr(seat, e), " + "))
fmt.Fprintf(b, "// Subject%sAll hears every %s.\nconst Subject%sAll = %q\n\n", ident(e.Name), e.Name, ident(e.Name),
"mesh.seat."+seat+".event."+e.Local())
}
// goParam is a part's name as a parameter: its words joined, the first lower-case.
func goParam(part string) string {
id := ident(part)
return strings.ToLower(id[:1]) + id[1:]
}
// partsPattern is the pattern a fixture's subject of an event with parts must match: a part is one token.
func partsPattern(seat string, e seatdef.Message) string {
toks := strings.Split(e.SubjectPattern(), ".")
for i, tok := range toks {
if strings.HasPrefix(tok, "<") {
toks[i] = `[^.*> ]+`
} else {
toks[i] = regexp(tok)
}
}
return "^" + regexp("mesh.seat."+seat+".event.") + strings.Join(toks, `\.`) + "$"
}
@@ -291,3 +291,49 @@ func TestATypeScriptContractIsJavaScriptWithItsDeclarations(t *testing.T) {
t.Error("the declarations do not import the part")
}
}
// TestADefinitionsActsSubjectsKeepAndCallersAreRead is design 47 G §89, §203, §260, §324, §331's check for the
// generator: it reads `acts` (a step on every, a language and `kind: image`), `subject`, `keep` and `callers`, and
// writes each event's subject and the stream `SEAT_<seat>_<event>` of one that names `keep`.
func TestADefinitionsActsSubjectsKeepAndCallersAreRead(t *testing.T) {
m, err := Load("testdata/contracts", "example.org/contracts/go", "@example/contracts")
if err != nil {
t.Fatal(err)
}
if acts := m.Seats["a-registry"].Acts; len(acts) != 3 || acts[0].On != "kind:image" || acts[1].On != "every" || acts[2].On != "go" {
t.Errorf("acts read as %+v", acts)
}
files, err := Generate(m)
if err != nil {
t.Fatal(err)
}
src := string(files["go/seats/aregistry/contract.go"])
for _, want := range []string{
`const StreamMilestoneAsked = "SEAT_a-registry_milestone-asked"`,
`const StreamOwed = "SEAT_a-registry_owed"`,
"func SubjectMilestoneAsked(milestone string) string {\n\treturn \"mesh.seat.a-registry.event.milestone-asked.\" + milestone\n}",
`SubjectMilestoneAskedAll = "mesh.seat.a-registry.event.milestone-asked.*"`,
`func SubjectOwed(repository, number string) string {`,
"func SubjectStepEnded(node string) string { return \"mesh.seat.a-registry.event.step-ended.\" + node }",
} {
if !strings.Contains(src, want) {
t.Errorf("the Go contract lacks %q:\n%s", want, src)
}
}
if strings.Contains(src, "StreamStepEnded") {
t.Error("an event naming no keep has a stream of its own")
}
js := string(files["ts/seats/aregistry/index.js"])
dts := string(files["ts/seats/aregistry/index.d.ts"])
for _, want := range []string{`export const milestoneAskedStream = "SEAT_a-registry_milestone-asked";`,
`export function stepEndedSubject(node) {`, `export const milestoneAskedSubjectAll = "mesh.seat.a-registry.event.milestone-asked.*";`} {
if !strings.Contains(js, want) {
t.Errorf("index.js lacks %q:\n%s", want, js)
}
}
for _, want := range []string{`export declare const owedStream: "SEAT_a-registry_owed";`, `export declare function owedSubject(repository: string, number: string): string;`} {
if !strings.Contains(dts, want) {
t.Errorf("index.d.ts lacks %q:\n%s", want, dts)
}
}
}
@@ -1,5 +1,10 @@
{"format":1,"seat":"a-registry","scope":"mesh","grain":"whole","domain":"Module","decision":"ADR 1","includes":["registry"],
"verbs":[{"name":"hold","input":{"type":"object"},"answer":{"type":"object"}}],
"verbs":[{"name":"hold","class":"read","callers":["invokers","persons","sessions","seat:b-registry","module:gitea","operator","healers","holder:node-backup@same-node"],"input":{"type":"object"},"answer":{"type":"object"}}],
"acts":[{"step":"build","on":"kind:image","bound":"60m"},{"step":"check","on":"every"},{"step":"build","on":"go"}],
"events":[
{"name":"milestone-asked","subject":"milestone-asked.<milestone>","body":{"type":"object"},"keep":{"max-age-days":90,"max-mib":16,"last-per-subject":true}},
{"name":"owed","subject":"owed.<repository>.<number>","body":{"type":"object"},"keep":{"max-age-days":7,"max-mib":64}},
{"name":"step-ended","subject":"step-ended.<node>","body":{"type":"object"}}],
"family":{"name":"a-registry.events","max-age-days":7},
"states":[
{"name":"asks","template":"state","description":"the asks held, one key per asker and ask",
@@ -110,6 +110,14 @@ func tsFiles(m *Module, out map[string][]byte) error {
fmt.Fprintf(&o.dts, "export declare const family: { readonly name: %q; readonly maxAgeDays: %d };\n\n", f.Name, f.MaxAgeDays)
}
for _, e := range d.Events {
if e.Keep != nil {
fmt.Fprintf(&o.js, "export const %sStream = %q;\n\n", tsName(e.Name), keptStream(d.Seat, e.Name))
fmt.Fprintf(&o.dts, "export declare const %sStream: %q;\n\n", tsName(e.Name), keptStream(d.Seat, e.Name))
}
if parts := e.Parts(); len(parts) > 0 {
tsPartsSubject(o, d.Seat, e, parts)
continue
}
tsSubject(o, tsName(e.Name), seatdef.EventSubject(d.Seat, e.Name), e.ByCaller)
}
for _, a := range d.Accepts {
@@ -235,3 +243,18 @@ func tsType(t *typ) string {
}
return "unknown"
}
// tsPartsSubject writes the subject of an event whose subject carries parts, as goPartsSubject does.
func tsPartsSubject(o *tsOut, seat string, e seatdef.Message, parts []string) {
args := make([]string, len(parts))
names := make([]string, len(parts))
for i, p := range parts {
names[i] = goParam(p)
args[i] = names[i] + ": string"
}
all := "mesh.seat." + seat + ".event." + e.Local()
fmt.Fprintf(&o.js, "export function %sSubject(%s) {\n return %s;\n}\n\n", tsName(e.Name), strings.Join(names, ", "), strings.Join(subjectExpr(seat, e), " + "))
fmt.Fprintf(&o.js, "export const %sSubjectAll = %q;\n\n", tsName(e.Name), all)
fmt.Fprintf(&o.dts, "export declare function %sSubject(%s): string;\n\n", tsName(e.Name), strings.Join(args, ", "))
fmt.Fprintf(&o.dts, "export declare const %sSubjectAll: %q;\n\n", tsName(e.Name), all)
}