AI-Companion Test Log¶
A running record of the human–AI collaboration validation exercise: can a fresh AI agent, given only the canonical AI-agent documentation (no tutorial walkthroughs), operate the MiniGraph Playground and help a human build correct knowledge graphs? Each stage raises the cognitive demand. The point is not just pass/fail — it is to surface where
llms.txt, the AI grammar (command-reference.md/minigraph-commands.json), and the human docs can be improved, and to harden the Rust port toward production quality along the way.Tracked as Open Thread
ot-companion-validation-sweepinmemory/continuity.md.
Method¶
- Fresh agent per test — a new sub-agent with no prior context, briefed only with a task and a
pointer to the canonical AI-agent docs. As of 2026-07-18 these are ported into THIS repo so a
test references the Rust port's own docs (which reflect its reality — e.g.
graph.jsretired, the synchronous companion endpoint):docs/llms.txt,docs/guides/knowledge-graph/ai-agent-guide.md,command-reference.md,minigraph-commands.json,skills-reference.md. Never the interactivehelp/*.mdor the tutorial walkthroughs. (Tutorials 1–4 used the Java upstream's copies.) From tutorial 6 on, the allowed set isdocs/llms.txt+ every guide it maps — including the layer-1/2 AI docs (docs/guides/event-script/,docs/guides/event-driven/, ported 2026-07-19) for tutorials whose extensions use flows and tasks. In-band engine lookups (help,describe skill) remain fair game but each one is recorded as a doc gap — the grammar's goal is that agents never need them. - The companion drives the live session over the documented endpoints. AI agents use the
synchronous
POST /api/companion/{id}/sync(outcome in-band{ok, output, error, result}; also teed to the human's WS console);GET /api/graph/session/{id}reads the model shape. - The human validates in the Playground UI (screenshots) and via a behavioral dry-run.
- The orchestrator (Claude Code) may privately consult the canonical tutorial to judge correctness and assist if the companion is genuinely stuck, but never leaks it into the briefing.
Validation ladder¶
| Level | Tutorials | What it tests | Briefing | Pause model | Acceptance |
|---|---|---|---|---|---|
| L1 — mechanics + transcription | 1–2 | drive the endpoints; look up exact syntax for a known graph | command list / syntax reference | step-wise | commands dispatch; graph built |
| L2 — comprehension | 3 | reconstruct a specified graph from the canonical docs alone (no walkthrough) | full node/connection plan; derive syntax | step-wise, screenshot each step | structural match to canonical + dry-run |
| L3 — synthesis / problem-solving | 4 | design a correct solution to a stated problem + output contract, with no syntax hints | problem statement only | build whole graph, pause for dry-run | behavioral: dry-run honors the contract on every branch |
| L4 — composition | 5 | compose mechanisms (parallel fan-out + join barrier + data sourcing + list assembly) from a problem + contract, no syntax hints | problem statement only | build whole graph, pause for dry-run | behavioral + demonstrable parallelism (traversal-log timing) |
| L5 — data-driven iteration | 6 | chained multi-step fetch where the fan-out set is runtime data (for_each inside a fetcher), incl. a POST provider |
problem statement only | build whole graph, pause for dry-run | behavioral: every element of the runtime list fetched + assembled; nothing hardcoded |
| L6 — mapping-language depth | 7 | the data-mapping mini-language end-to-end: constants, nested extraction, ordered array assembly, execution-time f: plugin values — pure transformation, no backends |
problem statement only | build whole graph, pause for dry-run | behavioral: exact reshaped contract incl. a genuinely runtime-generated value |
| L7 — reshaping toolbox | 8 | choose the right transformation tool (JSONPath wildcard extraction, f:listOfMap, f:removeKey) for a data-driven list-reshaping contract |
problem statement only | build whole graph, pause for dry-run | behavioral: contract met for any list length; internal field never exposed |
| L8 — reuse under governance | 9 | author common logic once in an off-path library module; borrow it at run time from the execution path (graph.math EXECUTE) |
problem statement only (reuse stated as an architectural requirement) | build whole graph, pause for dry-run | behavioral + structural: formula authored once, module off-path, borrower maps the result |
| L9 — composition by delegation | 10 | delegate to a deployed, governed graph model (graph.extension sub-graph) instead of rebuilding its logic |
problem statement only (deployed model's id + contract given as environment facts) | build whole graph, pause for dry-run | behavioral + structural: contract served with zero re-implementation (no own providers/dictionaries/fetchers) |
| L10 — cross-layer composition | 11 | delegate to a deployed Event Script flow (extension=flow://{flow-id}) — the semantic layer composing onto layer 2 |
problem statement only (flow id + contract as environment facts) | build whole graph, pause for dry-run | behavioral + structural: response produced by the flow (delegation visible in the traversal), zero echo logic in the graph |
| L11 — resilience engineering | 12 | custom error handling: exception= failure routing + a bounded retry loop (RESET/NEXT/DELAY, attempt counting) + a recovery path — under the engine's loop guard |
problem statement only (failure-simulation header as an environment fact) | build whole graph, dry-run BOTH paths | behavioral: failing path retries with pauses, recovers at the bound, ends in success; happy path untouched |
| L12 — the custom-logic seam | 13 | plug a deployed composable function into a graph (graph.task): whole-body pass-through, request headers, result mapping |
problem statement only (function route + contract as environment facts; header value differs from the doc example) | build whole graph, pause for dry-run | behavioral: the function's exact result returned; zero re-implementation |
Stage records¶
Tutorials 1–2 — L1 (mechanics + transcription) — PASSED¶
- Method: fresh companion agents derived the exact commands from
minigraph-commands.jsonand built the tutorial graphs, one command at a time with human screenshot validation. - Result: proved a fresh agent can (a) use the companion endpoint contract, (b) read the model back over the session endpoint, and (c) translate the command grammar into valid commands.
- Doc/grammar insight: the companion hint set should point at
docs/llms.txt+docs/guides/knowledge-graph/ai-agent-guide.md(the AI-facing docs), not the interactive help.
Tutorial 3 — L2 (comprehension) — PASSED¶
- Task: the Data-Dictionary method — source a person's name + address from an MDM Provider via
two Dictionary entries + a Fetcher, keyed by
person_id. - Method: fresh companion, canonical docs only, step-wise with a screenshot pause per node and per connection.
- Result: built 7 nodes + 7 connections, an exact structural match to the canonical
tutorial-3.json(verified by an independent nodes/edges diff); dry-run withperson_id=100returnedoutput.body = {name:"Peter", address:"100 World Blvd"}. - Loose ends surfaced (engine/parity), all fixed (increments 30–32):
@OptionalServiceonly gated#[preload]→ extended to#[websocket_service]/#[before_application]/#[main_application]; the Playground was retired from programmatic registration to declarative dev-gating (increment 31, Java parity).- The dev mock data providers (
mock.mdm.profile, …) weren't ported → added, dev-gated (increment 30). - A Dictionary node's bare
input[]=person_idwas wrongly rejected by the create/update console command → node-type-aware fix (Java first, then Rust). - Doc/grammar insight (increment 32): the
inspectcommand docs used{…}as a placeholder in the syntax line but repeated the braces in the examples, so the companion sentinspect {output.body}— which both engines resolve to the key{output→body}= empty. Fixed: examples unbraced, braces only in syntax lines, + a placeholder-convention note incommand-reference.md,ai-agent-guide.md,help inspect.md,minigraph-commands.json. A first pass also changed the webapp autocomplete template — reverted, because atemplatefield is a fill-in placeholder ({variable_name}is correct there), not an example. Lesson: in the docs,{…}must appear only where a value is substituted, never in text meant to be typed verbatim.
Tutorial 4 — L3 (synthesis / problem-solving) — FAILED → doc fix → RE-VERIFIED PASS¶
- Task (problem only, no syntax hints): input has two parameters
aandb; build a graph that compares them and branches two ways (a >= banda < b). Output contract:{ "message": <describes the outcome>, "less_than": <bool>, "sum": <a + b> }. Tutorial-4's focus is the decision node. - Method: fresh companion, canonical docs only, no node/type/mapping/
input.body.*hints; built the whole graph then paused for the dry-run (step-wise pauses dropped — mechanics proven at L2). - Design produced (companion's own — a valid alternative to the canonical
Decisionnode):root → compare (graph.math: sum + branch) → {ge-path | lt-path} (graph.data.mapper) → end (graph.data.mapper, assembles the contract). 5 nodes, 5 connections, all commands HTTP 200 — the build succeeded and read back exactly as intended. - Dry-run: FAILED.
a=5,b=3seeded fine (input.body={a:5,b:3}) but the run aborted at thecomparenode with the WS-console error:Walk to root→Walk to compare→node compare does not have if:, then: or else:→Graph traversal abortedRead-back confirmed total failure:model = {},output.body = Not found(not evenCOMPUTEran). The companion is blind to this — its POSTs returned HTTP 200 "accepted"; the parser error only reached the WS console, and its read-back endpoints just showed empty state. - Root cause — an AI-grammar gap. The companion invented
IF: … / BEGIN / NEXT: <relation> / END. The engine's realgraph.mathdecision grammar is the multi-line triadIF: <bool>/THEN: <node|next>/ELSE: <node|next>. Every wrong guess was one the docs caused:NEXT:actually takes a node name (not a relation label);BEGIN/ENDarefor_eachiteration delimiters (not IF-block braces); andTHEN:/ELSE:appear nowhere in the AI-facing docs. The AI docs (command-reference.md,minigraph-commands.json) listed the keywords (COMPUTE/IF/MAPPING/EXECUTE/RESET+NEXT:/BEGIN/END) but gave no statement syntax — the full grammar lived only in the interactivehelp graph-math.md, which the companion was (by design) forbidden to read. A keyword inventory without syntax was worse than silence:BEGIN/END/NEXT:were misleading breadcrumbs. - Doc fix (this session): promoted the
graph.math/graph.jsstatement grammar into the AI-facing docs — theIF/THEN/ELSEmulti-line form (with a worked branch example),NEXT:= node-name,BEGIN/END=for_eachscope,COMPUTE→resultnamespace,MAPPING/EXECUTE/RESET. Files:command-reference.md(new "statement grammar" subsection) +minigraph-commands.json(graph.mathstatement_syntax+ notes). (Java-canonical only — the Rust repo doesn't carry these guides; itshelp graph-math.mdalready had the full grammar.) - Verification — PASSED (fix proven). A second fresh companion (no knowledge of the failure or
the doc change), same problem, now read the documented grammar and built a runnable decision:
compare(graph.math) withCOMPUTE: sum -> a+b,COMPUTE: less_than -> a<b, and the real triadIF: a>=b / THEN: ge-path / ELSE: lt-path; twograph.data.mapperbranches filling the contract fromcompare.result.*. Dry-run on the primary session honored the contract on all three cases (types preserved —less_thana real boolean,suma real number8.0in graph.math double precision): a=5,b=3→{message:"a is greater than or equal to b", less_than:false, sum:8.0}a=3,b=5→{message:"a is less than b", less_than:true, sum:8.0}a=4,b=4→{message:"a is greater than or equal to b", less_than:false, sum:8.0}— the boundary handled more correctly than canonical tutorial-4 (which branches ona>b, soa==bwould wrongly take the "<" path); by computingless_than=a<band branching ona>=bthe companion nails the spec.- Additional (smaller) doc frictions from the retry, still open: (a) boolean/float/list constant
syntax (
boolean(...)etc.) isn't reachable from the four AI docs (deferred to the Event Script page) — the companion avoided it by computing the boolean; consider inlining the constant-type table; (b) no example of one node's mapper reading another node's.result(bare form,compare.result.sum); (c) whether anIFTHEN:/ELSE:jump requires a matchingconnect(and whether the relation label matters to routing) is unspecified; (d) a skill-less "terminate only"Endis only implied. None blocked the solution; all are polish. - Session-management feature validated in passing. For this run the human's primary
(
ws-145417-6) and a companion-side subscriber (ws-452385-7, a zero-dependency Node-22 built-in-WebSocket.mjs) were linked viasession subscribe. Content synced both ways (both showed 5 nodes / 5 connections) and the mirrored console stream gave the orchestrator live visibility into run output — directly mitigating finding #4: it let the orchestrator self-diagnose a curl-quoting bug of its own (zsh$"…"vs$'…') from the console alone, without the human relaying it. - Trivial aside: the first (failed) build showed as "untitled-1" — the root node had no
nameproperty (sets the graph title / export name); not the focus.
Tutorial 5 — L4 (composition: parallel fan-out + join + data sourcing) — PASSED (first attempt)¶
- Task (problem only, no syntax hints): fetch the profiles of TWO people in parallel (both API
calls at the same time) and return one combined response. Input:
{person1: <id>, person2: <id>}(mock ids 100/200). Profile API:GET /api/mdm/profile/{id}on the same server (the dev mock). Output contract:output.body = { "profile": [ <both profiles> ] }, each entry carrying at least name and address; order irrelevant. Tutorial-5's focus is parallelism + thegraph.joinbarrier. - Method: fresh companion; this repo's own AI docs for the first time (
docs/llms.txt+ the fourdocs/guides/knowledge-graph/files — tutorials 1–4 used the Java upstream's copies); build whole graph → dry-run; allowed to probe the profile endpoint directly and to use anything the engine says in-band; drove the human's live UI session (ws-976371-7) entirely via/sync. - Result: PASSED on the first run — 18/18 commands
ok:true, zero failed commands (a first for the sweep at L3+; tut-4 needed a doc fix + retry). Design:rootforks tofetch-one/fetch-two(graph.api.fetcher, per-fetchermodel.profile1/model.profile2scratch), both converge onjoin-profiles(graph.join), andend(graph.data.mapper) assemblesmodel.profile1 -> output.body.profile[0],model.profile2 -> output.body.profile[1]; one shared Dictionary (person-profile,response.profile -> result.profile) + Provider (mdm-profile,person_id -> path_parameter.id). Dry-run (verified independently by the orchestrator re-running it):{"profile":[{"address":"100 World Blvd","id":"100","name":"Peter"},{"address":"200 World Blvd","id":"200","name":"Mary"}]}. Parallelism proven from the traversal log: bothWalk to fetch-*lines precede eitherExecuted; the two fetches (~4.5–5 ms each) complete out-of-order inside a ~6 ms total traversal; the join executes twice (sink, then release). Exported asparallel-profile-fetch. - Judged against canonical
tutorial-5.json— semantically equivalent, three deliberate divergences, all valid and two arguably better: - One Dictionary returning the whole
response.profileobject vs canonical's two field-level entries (person-name,person-address) — fewer nodes for the same contract (each profile also carriesid, a superset). - Deterministic post-join assembly at
end(profile[0]/profile[1]) vs canonical's per-fetcher append (model.fetcher-N -> output.body.profile[], order undetermined) — avoids the concurrent-append hazard entirely and pins order (canonical's own walkthrough mentions the indexed variant as the order-guaranteeing option). - No organizational Island node — the docs mark it optional; Provider/Dictionary stayed
unconnected per the documented config-node exception.
Notably the companion derived tutorial-5's key lesson — parallel branches must write to
disjoint
model.*scratch keys — on its own, with no hint. - KEY method finding — the five AI docs alone were not sufficient; in-band help bridged the
gap. Provider URL
{name}placeholders and the Dictionary node's bareinput[](a parameter name, not asource -> targetmapping) are documented nowhere in the five docs — the page both guides delegate to (composing-the-layers.md#data-dictionary) doesn't exist in this repo. The companion resolved both before authoring —describe skill graph.api.fetcher(documented) told it to runhelp data-dictionary(undocumented but discoverable), whose in-band text carries the full recipe — and that is why it never issued a failing command. Legitimate under the rules (in-band is fair game, and the/syncchannel is exactly this feedback loop), but it means doc completeness was rescued by the engine's own help surface. - Doc frictions recorded by the companion (rollup #9–#13 — all fixed in the AI grammar the same
day, maintainer-directed): the dangling
links (
composing-the-layers.md,build-your-first-graph.md,playground-and-companion.md,index.md,../event-script/syntax.md— the latter owed the full constant-type set, tut-4 friction (a) again);help {topic}absent from the grammar docs; ai-agent-guide prose still calls the endpoint "/command" post-rename; the success envelope omitserror/result(increment-33 null-omission) though the docs writeerror: null, and carries an undocumentedidfield; fan-out concurrency (multiple outgoing connections = parallel branches) implied only bygraph.join's notes. - Engine finding by the orchestrator (not the companion):
session subscribeissued via/synccorrupts the subscriber registration. The command registers its reply route as the subscriber — through/syncthat is the ephemeral per-request capture route (companion.sync.<uuid>, released when the POST returns), not the session's real WS.out. Engine state confirmed it: the primary reportedsubscribed by ["companion.sync.3da2143d…"]while the watcher session believedsubscribed to ws-976371-7. (Post-mortem correction: the watcher's mirror feed dying mid-test was user-confirmed collateral of an accidental browser restart that closed/reopened the primary — not proven to be caused by this defect. The capture-route-as-subscriber state remains objectively wrong and worth fixing.) Java upstream mirrors the same/syncmechanism (#189) — check both ports. Workaround: issuesession subscribeover the WS connection itself. Tracked as an Open Thread inmemory/continuity.md. - RETEST (2026-07-19, after grammar fixes #9–#13 and #17–#22) — PASSED on the tightened
criterion: ZERO in-band lookups. A fresh companion, same problem-only brief, session
ws-876960-4: 20/20 commandsok:true, zero failures, first-attempt dry-run pass — and where the first run had to pulldescribe skill+help data-dictionaryfor Provider/Dictionary authoring, this run needed no in-band help at all: the docs alone sufficed (that is exactly what fixes #9–#13 claimed). Bonus validations: the companion wired the Island knowledge layer unprompted (root -[contains]-> knowledge -[data]-> person-profile -[provider]-> mdm-profile— the #22 mandate, applied from the grammar with no hint in the brief), cited the documented fork/state-safety rules (#13) in its design rationale, and produced an even cleaner design than the first run (each fetcher's result read directly from its own{node}.resultnamespace — no scratch keys needed — with deterministic post-joinprofile[0]/[1]assembly). Independently re-verified by the orchestrator (the island sinks in 0.014 ms; both fetchers overlap; contract honored). Two inference-only frictions noted (worked first try, but the docs never show them): array-index mapping targets (-> output.body.profile[0]) and whole-subtree Dictionary output (response.profile -> result.profile) — rollup #23/#24. Exported asparallel-profile-fetch-v2. - Session-hygiene aside (cause understood): the human's browser restarted accidentally before
the test; the primary session closed, a new one (
ws-976371-7) was handed over — and it arrived carrying a complete draft of this very exercise (the UI restores the local draft into the reconnected session;session resetresets subscriptions, not the draft graph). The orchestrator deleted all 7 nodes before briefing the companion, or the exercise would have been contaminated. Lesson for the method: always verify the primary is empty before briefing.
Tutorial 6 — L5 (data-driven iteration: for_each + chained fetch) — PASSED (first attempt)¶
- Task (problem only, no syntax hints): given
{person_id: <id>}, return the person's profile AND the details of every account they hold:output.body = {name, address, accounts:[detail…]}. Two live services:GET /api/mdm/profile/{id}(profile + the account-number list) andPOST /api/account/details(JSON bodyperson_id+account_id→ one account's details). The account list is runtime data — nothing hardcoded. Tutorial-6's focus isfor_eachiterative fetching (chained fetchers, parallel batches). - Method: fresh companion on the human's live session (
ws-876960-4), driven via/sync; docs =docs/llms.txt+ the guides it maps (first run on the post-#9–#13 grammar and the newly ported layer-1/2 docs); in-band lookups allowed but each recorded as a doc gap; the orchestrator's headless watcher subscribed over the WS connection (subscribed by ["ws-759054-5"]— the #194 read-only companion rule + WS-side subscribe validated live; the mirror held for the entire run). - Result: PASSED on the first dry-run — 18 observed POSTs, all
ok:true, zero failed commands (the companion reported 16 — it didn't count its twodescribe graphread-backs). Design:root → profile-fetcher (person-profile Dictionary → GET provider; name/address/accounts into disjoint model.* scratch) → accounts-fetcher (for_each[]=profile-fetcher.result.accounts -> model.account_id, explicitconcurrency=3, account-detail Dictionary → **POST provider** withbody.targets) → end (mapper assembles the contract). Dry-run returned all 5* account details + name/address for person 100; independently re-verified by the orchestrator (chained traversal ~15 ms; iteration inside the second fetcher — no join barrier needed, correctly). - Judged vs canonical
tutorial-6.json— semantically equivalent; economical divergences: one whole-profile Dictionary vs canonical's three field-level entries (name/address/accounts); scratchmodel.*+ deterministicend-mapper assembly vs canonical's direct-to-output.bodywrites; no organizational Island; nofeature[]flags (the mock needs none — canonical showsoauth2-beareras a placeholder). The key mechanism — a cross-node runtime array (profile-fetcher.result.accounts) drivingfor_eachiteration withmodel.*per-element input wiring — was derived correctly, with exactly one in-band lookup. - Grammar-sufficiency check (the tightened criterion from tut-5's planned retest, applied here):
the tut-5 fixes held — Provider/Dictionary authoring (incl. the previously-undocumented POST
body.*form) and everything else came straight from the AI docs, zero lookups. The single in-bandhelp graph-api-fetcherwas for thefor_eachidiom, whose authoring detail the AI docs still lack (rollup #17) — one gap class per tutorial, each narrower than the last. - Doc frictions recorded by the companion (rollup #17–#21 — all fixed same day,
maintainer-directed, engine-verified; plus the #22 island mandate):
for_eachidiom absent from the AI grammar (barefor_each[]=<array> -> model.<var>syntax only — no cross-node source example, no input-wiring pattern); aggregation semantics undocumented (per-iterationresult.{key}values collect into an array — confirmed only by experiment; ordering unspecified though observed input-order underconcurrency=3);output[]required-vs-optional contradiction forgraph.api.fetcher(AI matrix says required, engine help says optional); POST-body Provider authoring was example-free; and the KG grammar's constants section reads as a closed set whilef:simple-plugin calls (and$.JSONPath) are in fact resolvable in graph mappings — the graph skills route through the shared Event Script mapping engine (code-confirmed:skills.rs→get_lhs_or_constant→f:dispatch); the in-band help even recommendsf:as the:typereplacement.
Tutorial 7 — L6 (mapping-language depth) — PASSED (first attempt; the cleanest run yet)¶
- Task (problem only, no syntax hints): a pure transformation graph, no backends. Input
{profile: {name, address1, address2}}; output contract{hello: "world" (literal), name, address: [address1, address2] (ordered), time: <current local date-time generated at execution — not a constant>}. Tutorial-7's focus is the data-mapping mini-language: constants, nested extraction, array assembly, plugin functions. - Pre-flight (maintainer prompt): the tutorial needs the
f:now()simple plugin and not all Java plugins were assumed converted — verified present and correct in the port (registry +plugins_e8.rs+ a live scratch-session probe:iso/local/msall working in a real graph mapping) before briefing; no engine work needed. Also noted: the tutorial help pages carry known errors (backlog #16) — the canonical walkthrough itself recommends direct addressing over its own append+clear (model.none) sequence. - Result: PASSED — 8/8 commands
ok:true, zero failures, ZERO in-band lookups, first-attempt dry-run pass (sessionws-783755-2). Output verbatim:{"address":["100 World Blvd","New York"],"hello":"world","name":"Peter","time":"2026-07-19 08:54:28.815"}; independently re-run by the orchestrator (fresh timestamp proved execution-time generation). - The updated grammar visibly drove the design: indexed
address[0]/[1]chosen with the documented determinism-vs-append rationale (#23/#25 in action);f:now(text(local))discovered by following the #21 pointer from the KG grammar into the event-script plugin catalog (the cross-layer llms.txt map worked); nested seed lines (text(Peter) -> input.body.profile.name) inferred from the composite-key rule; and no cargo-cult island — the companion correctly reasoned the #22 mandate doesn't apply to a graph with no config/data-entity nodes. - Judged vs canonical
tutorial-7.json: semantically equivalent and more economical — 2 nodes (root→end-as-mapper, per the docs' own recipe) vs canonical's 3 (separate mapper + skill-less end); the companion independently landed on the walkthrough's own "better solution" (direct addressing, nomodel.noneclearing needed). - Frictions (inference-only, both fixed same day → rollup #26/#27): the KG grammar named no
example
f:plugin (an agent must realize the timestamp answer lives a layer down) → generator examples now inline; nested composite keys ininstantiateseed lines were implied but never shown → nested-seed example added.
Tutorial 8 — L7 (reshaping toolbox: JSONPath + transformation plugins) — PASSED (first attempt)¶
- Task (problem only, no syntax hints): pure transformation — input
{profile: {name, account: [{id, amount, description, type}, …]}}; output{name, account: [same accounts, same order, each WITHOUT the internal "description" field]}; list length data-driven, nothing hardcoded per element. Tutorial-8's teaching set: JSONPath wildcard extraction ($.…account[*].type→ map-of-lists),f:listOfMapconsolidation, andf:removeKey— the walkthrough's own "indeed easier" single-source route. - Result: PASSED — 17/17 commands
ok:true, zero failures, ZERO in-band lookups, first-attempt dry-run pass (sessionws-783755-2). Output verbatim:{"account":[{"amount":18000.3,"id":"100","type":"C/D"},{"amount":62050.8,"id":"200","type":"Saving"}],"name":"Peter"}— order preserved,descriptiongone; independently re-run by the orchestrator. - Technique chosen: the one-mapping
f:removeKey(input.body.profile.account, text(description))route — the canonical's own recommendation for a single source; the invocation form was inferred from the generic plugin pattern (→ #28). JSONPath/f:listOfMapwere not needed by the winning design (the JSONPath machinery itself remains covered by the canonical fixture suite); the companion's unused fallback plan was agraph.mathfor_eachloop (→ #29). - Emergent knowledge-layer behavior (beyond the #22 mandate): with no config nodes at all,
the companion still built an island with two data-entity nodes (
person-profile,account) whose purposes document the domain — including "description is internal-only and never exposed". The graph-as-living-documentation idea is now shaping designs unprompted; it also flagged that the mandate's scope for pure-transformation graphs could be stated crisply (→ #30, maintainer question). - Frictions: #28 (
f:removeKeyhad no syntax line/example anywhere — fixed same day, code-verified: map or list-of-maps, N key args, returns a copy) and #29 (graph.mathfor_each[]/BEGIN/ENDthinly specified — candidate; likely better addressed when a later tutorial exercises it). Verified-by-use with no issues: nested/indexedinstantiateseeds (#27's fix),double(…)constants, the/syncenvelope contract.
Tutorial 9 — L8 (reuse under governance: off-path modules + EXECUTE) — PASSED (one self-corrected iteration)¶
- Task (problem only; reuse stated architecturally, no mechanism named): input
{a, b}→ output{sum: a+b}, with the formula authored exactly once in a reusable library node off the execution path, borrowed at run time by a node on the path, and the graph readable as documentation (library distinguishable from flow). Tutorial-9's focus is the reusable module (graph.math+EXECUTE) — not subgraphs (graph.extensioncomes in later tutorials). - Result: PASSED — 19/19 commands
ok:true, zero failed commands, ZERO in-band grammar lookups; one functional defect self-corrected in-band (sessionws-783755-2). Firstrunproduced an empty output; the companion diagnosed it purely with threeinspects (model✓ →add-formula.resultempty →adder.resulthas the sum), identified the rule —EXECUTEresults land on the invoking node, not the module — fixed with oneupdate node, re-ran:{"sum": 30.0}. Independently re-verified by the orchestrator with different operands (7+35 → 42.0). Exported asreusable-addition. - Design matches canonical
tutorial-9.jsonin every structural idea: off-pathModule(add-formula, neutralmodel.*operands so any caller can feed it), execution-path caller (MAPPING×2 →EXECUTE→MAPPINGresult out), island anchoring the module (root -[contains]-> library -[module]-> add-formula) — the island applied to a module node unprompted, extending the #22/#30 convention exactly as the maintainer intends. - Findings: #31 — the
EXECUTEresult-landing rule (the canonical walkthrough's own teaching point: "it just borrows the logic") was absent from the AI grammar and cost the iteration — fixed same day (EXECUTE semantics + a full reusable-module worked example incommand-reference.md#math-statements, mirrored inminigraph-commands.json+skills-reference.md). #32 —#create's "{name}and{type}are lowercase-and-hyphen" contradicted the lexical table and every example (types Capitalized; engine accepts them) — fixed same day. #33 — island scope now explicitly includes module/library nodes, and the "never executed" phrasing corrected to match the run log ("executes only to sink") — fixed same day.
Tutorial 10 — L9 (composition by delegation: graph.extension sub-graph) — PASSED (first attempt)¶
- Task (problem only; the deployed model's id + contract given as environment facts): the
organization operates a deployed, governed graph model
tutorial-3(person_id → name+address, maintained by another team). Build a NEW graph that delegates the lookup — rebuilding, copying or re-implementing its internals is forbidden. Input{person_id}→ output{name, address}. Tutorial-10's focus is thegraph.extensionsub-graph. - Result: PASSED — 16/16 commands
ok:true, zero failures, ZERO in-band lookups, first-attempt dry-run pass (sessionws-783755-2). Output verbatim:{"address":"100 World Blvd","name":"Peter"}; independently re-run by the orchestrator withperson_id=200→ Mary (genuine delegation, nothing hardcoded). Exported asdelegate-profile. - The delegation choice was correct and unprompted:
graph.extensionwithextension=tutorial-3(notimport graph from tutorial-3, which would have copied the logic into the draft — exactly what the brief forbade); field-level result mapping (result.name/.address -> output.body.*); zero own providers/dictionaries/fetchers. The knowledge island (encouraged form, per #30) now documents the delegation itself: apersonentity plus aperson-lookup-modelnode carryinggraph_id=tutorial-3+ input/output contract properties — the external dependency captured as enterprise knowledge. - Frictions (inference-only, all fixed same day → #34–#36):
graph.extension's data contract was example-implied (bareinput[]targets = the sub-graph'sinput.bodykeys; the node'sresult.*is the sub-graph'soutput.body; whereextension={graph-id}resolves) — now rule-stated; the node-name lexical rule ("lowercase letters and hyphen only") wrongly excluded digits (canonical fixtures usefetcher-1; engine-verified: no charset validation) — corrected everywhere; the skill-lessEndwas only implied ("usually with graph.data.mapper") — now explicitly valid.
Tutorial 11 — L10 (cross-layer composition: flow extension) — PASSED (first attempt)¶
- Task (problem only; flow id + contract as environment facts): the platform hosts a deployed
Event Script flow
flow-11(an echo service, maintained by another team). Build a graph that hands its input to that flow and returns the flow's response — implementing the echo in the graph is forbidden. Input{hello, message}→ output = the flow's echo. Tutorial-11's focus isextension=flow://{flow-id}— the semantic layer composing onto layer 2. - Pre-flight:
flow-11verified live in the running app by an orchestrator scratch-session probe before briefing (echoed{"hello":"probe"}through the flow). - Result: PASSED — 15/15 commands
ok:true, zero failures, ZERO in-band lookups, first-attempt dry-run pass (sessionws-783755-2). Output verbatim:{"hello":"world","message":"this is a good day"}; delegation proven in-band (Executed flow-delegate with skill graph.extension;inspect flow-delegate.result= the flow's response); independently re-run with a different payload. Exporteddelegate-to-flow. - The feedback loop is compounding — the companion explicitly credited the prior fixes: "the
island convention, the
flow://prefix, the bare-keyinput[]semantics, and theresult-is-flow-output rule were all stated as rules (not just examples), which is exactly what made a zero-error first run possible" (#22/#30, #34, #36 at work). The island documents the flow dependency (echo-flowentity withflow_id+ contract + ownership note). - Frictions (minor): #37 — whole-body
*forwarding ongraph.extensionunspecified — engine-verified: not supported (named keys only; the*merge idiom isgraph.task-only) and now stated; #38 — no discovery command for deployed flow/graph ids (agents rely on the brief) — candidate, needs engine work; #39 — theDescribed in /api/graph/model/{name}/{token}reply fromexportreferenced an undocumented endpoint — now noted in#export.
Tutorial 12 — L11 (resilience engineering: exception routing + bounded retry + recovery) — PASSED (first attempt)¶
- Task (problem only; the
X-Exception: truefailure-simulation header as an environment fact): a resilient person lookup — input{person_id, exception?}; withexception=truethe service call fails (401) and the graph must NOT abort: bounded retries (max 3) with a pause between attempts, then a recovery path that clears the flag and fetches successfully; happy path straight through. The most demanding exercise of the sweep — it composesexception=failure routing, agraph.mathretry loop (f:defaultValue/f:addcounting,IFbound check,RESET,NEXT,DELAY) and the engine's loop guard. - Pre-flight: the canonical
tutorial-12was scratch-probed end-to-end first (retry machinery verified live in the Rust engine — it is not among the fixture tests). The probe also surfaced #40:import graph from tutorial-12returnedok:falseon a command that succeeded — the/syncis_error_lineheuristic trips on import's benign "Graph model not found in /tmp/…" fallback line. - Result: PASSED — 27/27 commands
ok:true, zero failures, ZERO in-band lookups, both dry-runs first attempt (sessionws-783755-2). Failing path: 4 fetch executions (3 real failures — the handler is reachable only via the exception route), two visible ~500 ms pauses,attempts: 3.0, flag cleared, final success{Peter, 100 World Blvd}; happy path: one call, 7 ms, retry machinery untouched. Independently re-run with person 200 (4 executions → Mary). Exportedresilient-profile. - Design judged vs canonical
tutorial-12.json: equivalent mechanics, arguably cleaner decomposition — a three-node retry machine (retry-control: count + bound →retry-pause: DELAY + resets + NEXT →recover: clear flag + refetch) using mutual resets instead of canonical's comma-list self-reset; no defensive status check needed (the handler only runs on failure); the same Dictionaryexception:falsedefault + Providerheader.x-exceptionpass-through as canonical;initused default-then-overlay in place of canonical'sf:defaultValue(both now documented idioms). - Frictions — seven load-bearing behaviors were inferred, not documented (each "a gamble" that
paid off): #41–#46, all engine-verified and fixed same day.
exception=semantics (jump not abort;output[]skipped; no-handler ⇒ abort) → new Failure routing section with the canonical bounded-retry pattern; RESET truths (comma list, safe no-op on never-executed nodes, self-reset allowed, loop-guard warning) → statement grammar; taken-IF-jump ends the statement list whilenextfalls through → statement grammar;NEXT:/DELAY:arestatement[]lines (NEXT applies after the list completes — last one wins; DELAY pauses after the node) → keyword block rewritten; unresolvable mapping source skips the entry (target untouched — enables default-then-overlay) → mapping rules; dedup scope = per graph instance, successful responses only (failures never cached ⇒ retries re-call) → fetcher gotchas.
Tutorial 13 — L12 (the custom-logic seam: graph.task) — PASSED (first attempt) — SWEEP COMPLETE¶
- Task (problem only; the function's route + contract as environment facts): plug the deployed
composable function
v1.hello.task(greeting fromname, doublesamount, echoes thex-appheader) into a graph — whole input payload as its request body, thex-appheader set toportfolio-app(deliberately different from the docs' example value, so the header idiom must be understood, not copied), the function's result asoutput.body. Pre-flight: the demo function verified ported (mock.rs,#[preload]+ first-class#[optional_service], Java-contract parity, fixture-tested). - Result: PASSED — 16/16 commands
ok:true, zero failures, ZERO in-band lookups, first-attempt dry-run pass (sessionws-783755-2). Output verbatim:{"app":"portfolio-app","doubled":42.0,"greeting":"Hello, world"}; independently re-run with different values (Eric/50→Hello, Eric/100.0). Exportedhello-task-app. The island documents the capability (hello-servicenode: route + contract) and the response entity. - Frictions (both fixed same day → #47/#48): bare-
resultsemantics forgraph.taskwere shown-not-stated (now the explicit rule, aligned withgraph.extension); the example's "(Tutorial 13)" label pointed outside the self-sufficient doc set (relabeled).
Sweep complete — the scorecard (2026-07-19)¶
All 13 tutorials passed (plus the tutorial-5 retest), every one by a fresh agent with no prior context, from the AI docs alone, over the synchronous companion endpoint, with a human watching live. The arc:
| Phase | Tutorials | Outcome |
|---|---|---|
| Java-docs era | 1–4 | mechanics → comprehension → synthesis; 2 doc-fix cycles (incl. the tut-4 FAIL → statement-grammar fix → re-verified PASS) |
| This repo's docs, first runs | 5–6 | first-attempt passes; 2 then 1 in-band lookups — gaps #9–#22 fixed |
| Self-sufficiency proven | 5-retest, 7–13 | eight consecutive zero-lookup, first-attempt passes (tut-9's single functional defect self-corrected in-band) |
What the sweep produced beyond validation: 48 rollup findings (all resolved or tracked);
the synchronous companion endpoint (/sync, both ports, byte-identical); the read-only companion
session rule (#194); #[optional_service] as a first-class attribute (increment 34); the
layer-1/2 AI-doc port; the island knowledge-layer mandate; and an AI grammar hardened to the
point where fresh agents design cleaner-than-canonical solutions and cite the rules they
build on. Open follow-ups live in memory/continuity.md: the help-pages rewrite backlog
(closed 2026-07-19), the flow/graph discovery-command backlog, and the /sync ok-heuristic
engine fix (#40 — closed 2026-07-19, both ports).
Post-sweep forced test-drive: f:listOfMap (2026-07-19)¶
The one reshaping plugin the sweep never exercised (tut-8's agent chose the "easier"
f:removeKey route) got its own fresh-agent validation — a custom exercise whose contract
requires real impedance matching and cannot be solved with hard-coded indices.
- Setup (fully headless — no human session needed): orchestrator started the dev server,
opened a Playground session over a zero-dep raw-WebSocket client (
ws-301207-1,/ws/graph/playground), verified the draft empty, and dropped a column-oriented export at/tmp/columnar-catalog.json({title[], price[], sku[], internal_cost[]}— 4 rows). - Brief (L3, problem-only): ingest the nightly columnar export; answer row-oriented
(
output.body.books= row objects with EXACTLYtitle/price/sku, in row order);internal_costis confidential and must not appear;output.body.count; the export length varies run to run (kills any hard-coded-index bypass). No plugin names, no syntax. - Result: PASSED — 17/17 commands ok, ZERO in-band lookups, first-attempt dry-run pass.
The design is arguably better than the intended solution: one
normalizermapper chainsfile(json:…) -> model.catalog→f:removeKey(…, text(internal_cost))→f:listOfMap(…) -> model.books— dropping the confidential column before rows are ever formed — and theendmapper addsf:length(…) -> output.body.count. The island knowledge layer was volunteered unprompted with data-entity nodes documenting the export and the public-vs-confidential field split (#22/#30 shaping designs again). - Judged first-hand (not from the agent's report): orchestrator re-ran
instantiate/run/inspect — rows ordered, field sets exact,
internal_costabsent from the wholeoutputnamespace, count 4; then swapped the export to 2 books and re-ran: count 2, new titles — proving length-agnosticism AND thatfile(json:…)is read at execution time. The WS console tee carried all 45 lines live. - Findings #49–#52 (all engine-verified and FIXED same day, below). The agent's four "inferences that held" were exactly the four rules the docs didn't state.
| # | From | Insight | Action |
|---|---|---|---|
| 49 | listOfMap drive | mapping[] in-order application is implied, not rule-stated — the chain idiom (a later entry reads an earlier entry's target) rests on it |
DONE (same day, engine-verified: sequential entry loop in the mapper) — rule stated in skills-reference.md#data-mapper, the JSON catalog's mapper entry, and help graph-data-mapper.md |
| 50 | listOfMap drive | f:listOfMap order preservation shown, never guaranteed in the text |
DONE (same day, engine-verified: normalize_map_of_lists builds row i from index i) — "order-preserving, length-agnostic" stated in syntax.md (matrix row + prose) and the KG catalog |
| 51 | listOfMap drive | file(…) resolution time unstated — matters for a "varies run to run" ingest |
DONE (same day, empirically verified: swapped the file between runs, next run picked it up) — "read at mapping-evaluation time (each execution)" stated in the constants table + catalog |
| 52 | listOfMap drive | f:length only discoverable in the Layer-2 catalog (cosmetic; cross-layer discovery via llms.txt worked as designed) |
DONE (same day) — added to the KG grammar's f: examples cell |
The self-sufficiency streak extends: nine consecutive zero-lookup first-attempt passes (tut-5 retest → tut-13, plus this forced drive).
Post-sweep forced test-drive #2: discovery-driven delegation (2026-07-20)¶
The capstone for increment 42's discovery commands and the completed human-docs site: a
delegation brief that names no graph or flow id — the agent must find its extension=
target itself.
- Setup: headless orchestrator-owned session (
ws-575720-1), empty draft verified. - Brief (L3+, problem-only): "a teammate already deployed a profile-lookup capability
somewhere on this server — you don't know its name; delegate to it (no own
Provider/Dictionary/fetcher) and answer
greeting: Hello {name} of {address}; validate for person 100 AND 200." The public execution endpoint was explicitly allowed for investigation. - Result: PASSED — 21/21 commands ok, ZERO in-band lookups, first-attempt dry-runs for
both persons. Discovery loop exactly as designed:
list graphs→ two candidates by purpose (tutorial-3/tutorial-5 share the same text) → empirical contract probes viaPOST /api/graph/{id}→extension=tutorial-3delegation withf:concatgreeting assembly, plus an unprompted island documenting the delegation contract itself (DataEntity + ExternalModel node with graph_id/contract properties). - Judged first-hand: orchestrator re-ran the dry-run (greeting exact for person 100) and
inspected the exported model (
extension=tutorial-3, zero own fetchers). Then probed the agent's defensive inference #56 empirically:f:args DO resolve{node}.result.*— the staging throughmodel.*was unnecessary caution, now documented away.
| # | From | Insight | Action |
|---|---|---|---|
| 53 | discovery drive | No contract visibility for deployed models — list graphs gives id+purpose but not input/output contracts; the model view needs an exporter-only token, so the agent probed POST /api/graph/{id} empirically (worked, but trial-and-error) |
DONE (2026-07-20, increment 47, both ports): new read-only describe graph {graph-id} — a deployed model's contract view (purpose, size, input.*/output.* data surface derived from its own mappings); list graphs footer advertises it; grammar/catalog/help/agent-guide updated |
| 54 | discovery drive | Identical purposes don't disambiguate — tutorial-3 and tutorial-5 carry the same purpose text | DONE (2026-07-20, increment 47, both repos): tutorial-3 = "fetch one person profile (name and address) by person_id"; tutorial-5 = "parallel data sourcing composition - fan out two profile fetches and join the results"; help tutorial 3 transcript synced |
| 55 | discovery drive | text() whitespace preservation was undocumented (the agent trusted it for text(Hello )) |
DONE (same day) — "spaces preserved exactly" stated in the constants table + catalog |
| 56 | discovery drive | whether f: plugin args can read {node}.result.* was unstated (agent staged defensively through model.*) |
DONE (same day, empirically verified: f:concat(text(Hi ), profile-lookup.result.name) works) — "args accept any state-machine path" stated in grammar + catalog |
| 57 | discovery drive | re-instantiate semantics unstated (does it reset prior run state?) |
DONE (same day, verified across repeated cycles) — "replaces the current instance: fresh state machine, cleared run marks" stated in grammar + catalog |
Streak: ten consecutive zero-lookup first-attempt passes.
Post-sweep forced test-drive #3: live HTTPS fetch (2026-07-20)¶
The field-validation of increment 48 (outbound HTTPS for the async HTTP client): the
strict OS-trust-store path can't be proven with a self-signed unit fixture, so a fresh agent
drives the whole stack — Fetcher → async.http.request → rustls → a real CA-signed chain
on the public internet (maintainer-proposed: systems of record in the field require HTTPS).
- Setup: headless orchestrator-owned session (
ws-192994-1), empty draft verified. Pre-flight (scratch sessionws-655120-2, per the tut-7f:nowprecedent): the transport was probed once before briefing —https://www.google.com/returned 200 with an 82,777-byte page through the graph, proving TLS against the live chain; the naked domain answers 301 (body captured, traversal proceeds). The scratch session was closed before the brief. - Brief (L3, problem-only): "build a graph that fetches the page at
https://google.comand returns, in the graph's output, the HTTP status of the fetch and the size of the page; prove it with a dry-run." No skill names, no redirect hint, no syntax. - Result: PASSED — 16/16 commands ok, ZERO in-band lookups, first-attempt dry-run.
Textbook data-dictionary design (Provider + Dictionary + fetcher + island, all unprompted):
output[]=response -> result.page(whole raw HTML body),page-fetcher.status -> output.body.status,f:length(model.page) -> output.body.page_size. The agent handled the redirect truthfully:https://google.comanswers301with a 220-char body, and that is exactly what its graph reports ({status: 301, page_size: 220}) — it reasoned in its report that 301 < 400 is a non-failure and the fetcher reports the actual fetch. It even re-counted the body characters outside the engine to verifyf:length. - Judged first-hand: the WS console tee carried the full transcript; the orchestrator's own pre-flight independently confirmed both the 200/www and 301/naked behaviors. HTTPS is field-validated end-to-end in the Rust port.
- Findings #58–#61 (agent ambiguity reports — all engine-verified and doc-FIXED same day) + #62–#63 (orchestrator pre-flight, engine-behavior candidates for the maintainer, both ports).
| # | From | Insight | Action |
|---|---|---|---|
| 58 | https drive | bare response root as a Dictionary output source was undocumented — examples showed response.{path} only; the whole-raw-body form is the natural way to keep a non-JSON body (HTML/text) whole; the agent inferred it |
DONE (same day, engine-verified: fetcher.rs explicitly allows lhs == "response") — stated in command-reference.md#provider-dictionary, the catalog's Dictionary output note, skills-reference.md#api-fetcher, help graph-api-fetcher.md |
| 59 | https drive | f:length semantics beyond lists unstated ("element count of a list") — string behavior was an inference; chars-vs-bytes undocumented |
DONE (same day, engine-verified in conversions.rs::get_length, Java-parity doc comment): list → element count, string → character count, bytes → byte count, null → 0 — stated in the KG grammar's f: examples cell |
| 60 | https drive | {node}.status on success was unguaranteed in the docs — the namespaces table said "set by the engine" but only failure routing (≥ 400) documented it concretely; whether a 200/301 sets it was unverified |
DONE (same day, engine-verified: fetcher.rs sets {node}.status unconditionally for every executed fetch, before the ≥ 400 branch) — guarantee stated in the namespaces table, fetcher docs, and catalog |
| 61 | https drive | redirect behavior + response-metadata reachability undocumented — the fetcher never follows redirects (one Provider call = one HTTP request); response.* is body-only; status lives at {node}.status, headers at {node}.header.response.* via feature[]=log-response-headers |
DONE (same day, engine-verified in fetcher.rs + features.rs) — "HTTP semantics" block in skills-reference.md#api-fetcher + rules in command-reference.md#provider-dictionary, catalog, help graph-api-fetcher.md |
| 62 | https drive (orchestrator) | silent 1-second identical-command dedup on the command surface, BOTH engines (is_duplicate / GraphCommandService): an identical command repeated within 1s on the same session is dropped with only a debug log — a /sync caller gets ok:true with EMPTY output and no explanation, violating the documented envelope (echo always present). A scripted AI caller pipelining commands hits this; LLM pacing masked it through the whole sweep |
DONE (2026-07-20, increment 49, both ports): /sync marks its dispatch direct and the guard does not apply — repeats always execute; the WS path keeps the guard unchanged (tested both ways in graph_runtime.rs + CompanionSyncTest); envelope contract documented in the agent guide + catalog |
| 63 | https drive (orchestrator) | /sync ok:true on syntax-usage rejections — a malformed command (e.g. connect a to b with type x) answers with the Syntax: … usage line, which first_error_line doesn't classify as an error → false-positive ok:true (the command did nothing). Mirror image of #40's false-negative |
DONE (2026-07-20, increment 49, both ports): Syntax: classifies as the error line — ok:false with the hint in-band (no help page starts a line with Syntax:, so no inverse false-positive); red/green-verified on old code (Rust) |
Streak: eleven consecutive zero-lookup first-attempt passes.
Back-port validation: the JAVA engine on the back-ported docs (2026-07-20)¶
Graduation prep: the survey found the Java AI docs frozen at the pre-tut-5 state (31/40
grammar-hardening markers absent), so the hardened grammar was back-ported to the Java repo
(branch docs/ai-grammar-backport: 4 KG AI docs + llms.txt + event-script syntax.md +
help graph-api-fetcher.md + the parked error.status→error.code flow-schema fix;
graph.js restored as a live skill — its retirement is Rust-only). The gold test: a fresh
agent drives the Java Playground (example app :8085, built from the branch) from the
back-ported Java docs alone, on the columnar f:listOfMap exercise (drive-#1 design).
- Result: PASSED — 17/17 commands ok, zero failures, ZERO in-band lookups, first-attempt dry-run — the first fresh-agent validation of the JAVA engine on the hardened grammar.
- Same design quality as the Rust drives: allowlist projection (rows exact by construction —
stronger than the removeKey route), in-order chain idiom,
f:listOfMaporder guarantee,f:lengthon the published list, skill-less End, island volunteered with entities documenting the confidentiality split and apublished-asrelation. - The agent ran the variable-length proof unprompted: appended a 5th row, re-instantiated
the unchanged graph → count 5 in order; export restored byte-identically (
cmp) — exercising the documented "file() read at mapping-evaluation time" rule for real. - Observations (no fixes filed; agent's own judgment: "no doc-gap issue worth filing"):
the
f:listOfMapworked example's nested-wrapper input reads ambiguously; row-object JSON key order is unspecified (correctly inferred as unconstrained). - Orchestrator side-finding (headless-client authors): the Java WS server counts only
application-level text frames as activity — a client sending protocol-level pings alone is
idle-closed (~60 s); the Java UI's
{"type":"ping","message":"keep alive"}text frame is the keepalive contract. The Rust port tolerates protocol pings (more lenient, not a bug).
The streak is now twelve consecutive zero-lookup first-attempt passes — the twelfth on the Java engine, proving the hardened grammar is engine-neutral.
Findings → documentation & grammar improvements (rollup)¶
| # | From | Insight | Action |
|---|---|---|---|
| 1 | Tut 1–2 | AI agents need the AI-facing docs, not interactive help | hint set = llms.txt + ai-agent-guide.md |
| 2 | Tut 3 | {…} mixed as placeholder and in examples misleads a literal reader/AI |
unbrace examples; placeholder-convention note (increment 32) |
| 3 | Tut 4 | AI docs list graph.math statement keywords but give no statement syntax (no IF/THEN/ELSE, NEXT:/BEGIN/END semantics unstated) → a capable agent invents wrong-but-plausible branching that fails silently |
DONE + verified — documented the graph.math/graph.js statement grammar in command-reference.md + minigraph-commands.json (IF/THEN/ELSE, NEXT:=node-name, BEGIN/END=for_each, COMPUTE→result); a fresh companion then solved Tut-4 from the problem alone |
| 4 | Tut 4 | companion is blind to run-time errors — POST returns 200, the parser error only reaches the WS console, read-back shows empty state | DONE (Rust prototype, verified end-to-end) — additive synchronous POST /api/companion/{id}/sync returns {ok, output, error, result} in-band (design: docs/design/ai-companion-sync.md; blueprint bp-companion-sync). Re-verified: a fresh companion rebuilt tutorial-4 fully autonomously via /command (built + ran + self-validated all 3 cases; explicitly never needed the WS console or a GET fallback); errors return in-band (ok:false + error). Java upstream PR still to follow. |
| 5 | Tut 4 | boolean/float/list constant syntax (boolean(...), etc.) isn't reachable from the four AI docs (deferred to the Event Script page) |
(candidate) inline the constant-type table into the knowledge-graph grammar |
| 6 | Tut 4 | no example of a mapper reading another node's .result; IF jump ↔ connect relationship unspecified; skill-less terminal End only implied |
(candidate) add small examples/notes to the skills/command reference |
| 7 | Tut-4 live demo | graph.js retired at runtime but still listed as available in the AI docs → a fresh companion wasted three commands trying it (though it self-corrected each time via the in-band error) |
DONE — AI docs ported into this Rust repo (docs/guides/knowledge-graph/ + docs/llms.txt) and graph.js marked retired everywhere (command-reference, minigraph-commands.json, skills-reference); future tests reference the Rust repo's docs |
| 8 | Tut-4 live demo | graph.math's dialect is narrower than "JS-like" implies (no bitwise ops, no function calls) and serializes integers as floats (8.0) with no in-grammar coercion |
DONE — documented in the ported command-reference + skills-reference |
| 9 | Tut 5 | Provider URL {name} placeholder syntax and the Dictionary node's bare input[] shape (incl. : = default value only) were in none of the five AI docs — the page they delegated to (composing-the-layers.md#data-dictionary) doesn't exist in this repo; the recipe lived only in in-band help data-dictionary |
DONE (same day) — new Provider & Dictionary section in command-reference.md, enriched config_nodes in minigraph-commands.json, skills-reference.md retargeted; all dangling links removed and the closed constant set inlined (#constants; deprecated :type suffix explicitly excluded — maintainer confirmed) |
| 10 | Tut 5 | help {topic} is the engine's own discovery surface (in-band describe skill even points to it) but was absent from the AI grammar docs |
DONE (same day) — help documented as a command in command-reference.md + minigraph-commands.json (topics, aliases) |
| 11 | Tut 5 | ai-agent-guide.md prose still called the synchronous endpoint "the /command endpoint" after the /command → /sync rename (tables/examples were correct) |
DONE (same day) — prose swept to /sync |
| 12 | Tut 5 | the /sync success envelope omits error/result (increment-33 serializer null-omission) though the docs wrote error: null | "...", and carries an undocumented id field — a strict parser trips |
DONE (same day) — envelope documented truthfully (absent ⇒ null, id field) in ai-agent-guide.md + a sync_envelope object in minigraph-commands.json |
| 13 | Tut 5 | fan-out concurrency (multiple outgoing connections fork parallel branches) was implied only by graph.join's notes |
DONE (same day) — explicit fork/join/state-safety rules in command-reference.md#connect, skills-reference.md#join, and a traversal object in minigraph-commands.json |
| 14 | Tut 5 (orchestrator) | engine defect, both ports: session subscribe via /sync registers the ephemeral companion.sync.<uuid> capture route as a durable subscriber → dangling subscriber + asymmetric session state. (The mirror death observed mid-test was later user-confirmed as collateral of an accidental browser restart — the wrong-registration defect stands on the engine-state evidence alone.) |
DONE (Rust, same day; maintainer decision): both companion endpoints now limit session to the read-only status query — subscribe/unsubscribe/reset are rejected before dispatch (a companion is an assistant to a session, not a WS session of its own); refusal returned in-band (ok:false on /sync, 400 on fire-and-forget) and teed to the live console; deterministic test in graph_runtime.rs; AI docs updated. Java fix MERGED (Accenture/mercury-composable#194): same guard in PostCompanionCommand/-Sync + shared statics in GraphCommandService, byte-identical refusal text, test companionEndpointsLimitSessionCommandToReadOnly, 65-test module suite green — the read-only rule is live in both engines. |
| 15 | Tut 5 (orchestrator) | session reset resets subscriptions but does not clear the draft graph, and the UI restores the local draft into a reconnected session — a "fresh" session can carry a stale (here: exercise-contaminating) draft |
(candidate) UX note in help session.md; consider a clear graph affordance; method rule: verify-empty before briefing |
| 16 | Tut 5 (maintainer) | the interactive help/*.md pages are written for human operators and double as the engine's in-band reference; after tut-5 the AI grammar is self-sufficient, so the help pages deserve a dedicated human-UX rewrite (clarity, structure, the #15 session reset note) |
Backlog — Open Thread ot-help-pages-rewrite in memory/continuity.md; separate session; coordinate with the Java upstream (the pages are verbatim ports) |
| 17 | Tut 6 | the for_each authoring idiom was absent from the AI grammar — only the bare for_each[]=<array> -> model.<var> syntax appeared; no cross-node array source ({fetcher}.result.{key}), no input[]=model.<var> -> {param} wiring pattern — the companion's single in-band lookup (help graph-api-fetcher) was for exactly this |
DONE (2026-07-19, maintainer-directed) — new Iterative fetching section (source, wiring, concurrency, worked example) + for_each object in minigraph-commands.json + skills-reference.md#api-fetcher |
| 18 | Tut 6 | for_each result aggregation was undocumented everywhere (incl. the engine help): per-iteration result.{key} values collect into a single array; ordering under concurrency unspecified — the companion confirmed by experiment |
DONE — engine-verified (fetcher.rs: batches execute in input order, responses join in request order, results []-append per response ⇒ order deterministically follows the source list) and documented as a guarantee in all three docs |
| 19 | Tut 6 | output[] contradiction for graph.api.fetcher: the AI skill matrix + minigraph-commands.json said required; the engine help says optional ("use another data mapper") |
DONE — engine-verified (only dictionary[] is hard-required; the result always lands at {node}.result): matrix + JSON now say dictionary[] required, input[] conditional (when dictionaries declare parameters), output[] optional. The engine help contradicts itself on this — folded into the help-rewrite backlog (#16) |
| 20 | Tut 6 | POST-body Provider authoring was example-free — body.{key} targets + content-type header were stated only as a target list; the only worked example was GET/path_parameter |
DONE — second worked example (POST + body.* + content-type) in command-reference.md#provider-dictionary |
| 21 | Tut 6 | the KG grammar's constants section read as a closed set, but f: simple-plugin calls and $. JSONPath are resolvable in graph mappings (shared Event Script mapping engine; code-confirmed) — and the engine help recommends f: as the :type replacement |
DONE — "non-constant source forms" table added under #constants (pointing at the event-script plugin catalog) + _non_constant_sources note in minigraph-commands.json |
| 22 | Tut 6 (maintainer) | graph.island is required, not optional: islands are isolated from traversal but they link data entities and dictionaries into the graph's entity-relationship diagram — the graph is living documentation of enterprise knowledge (a new joiner discovers the domain model from the connected dictionaries/entities). The tut-6 companion left its four config nodes floating (the docs said "optionally group") |
DONE (2026-07-19, maintainer decision) — the AI grammar now mandates no node left unconnected: command-reference.md#island (new section), invariants + node-types + #provider-dictionary reworded, graph.island rewritten in skills-reference.md, pre-send checklist + recipe step in ai-agent-guide.md, minigraph-commands.json (_role, invariants, island notes). Verified by the tut-5 retest: a fresh companion wired the island unprompted |
| 23 | Tut-5 retest | array-index mapping targets were never shown — the lexical rules said keys "may be composite (dot-bracket)" but no doc had a worked example of building a JSON list in a mapper target (… -> output.body.profile[0]), the natural post-join assembly idiom; the companion inferred it correctly |
DONE (2026-07-19) — "Composite keys & arrays" block in command-reference.md#namespaces + indexed-assembly example in skills-reference.md#data-mapper + composite_keys lexical note in minigraph-commands.json |
| 24 | Tut-5 retest | whole-subtree Dictionary output mapping was only implied — examples mapped leaf paths (response.profile.name -> result.name); that an interior path (response.profile -> result.profile) yields the entire subtree was never stated; the companion inferred it correctly |
DONE (2026-07-19) — leaf-vs-interior rule + examples in command-reference.md#provider-dictionary (and the composite-keys block) + the Dictionary output note in minigraph-commands.json |
| 25 | maintainer | the [] array-append target (… -> output.body.profile[] appends an element, creating the list with the first element when absent) was absent from the KG grammar — and the parallel state-safety wording ("write to disjoint keys") over-restricted: data mapping is thread-safe, so concurrent appends carry no racing risk (element order follows completion order) |
DONE (2026-07-19, engine-verified: mlm.rs appendIndex parity; the state mutex serializes mapping ops) — append + thread-safety + ordering guidance in the composite-keys block, #connect state-safety refined, graph.join gotcha aligned, array_append note in minigraph-commands.json |
| 26 | Tut 7 | the KG grammar named no example f: plugin — an agent needing an execution-time value (timestamp, uuid) must realize the answer lives a layer down in the event-script plugin catalog; the pointer existed but carried no scent |
DONE (same day) — generator/arithmetic/logic examples (f:now(text(local)), f:uuid(), …) inlined in the #constants non-constant-sources row + minigraph-commands.json |
| 27 | Tut 7 | nested composite keys in instantiate seed lines were implied by the global lexical rule but never shown — every example seeded flat keys |
DONE (same day) — nested-seed example (text(Peter) -> input.body.profile.name) in command-reference.md#instantiate + a note in minigraph-commands.json |
| 28 | Tut 8 | f:removeKey had no syntax line or worked example anywhere (the plugin table only said "remove one or more keys from a map or list of maps") — the companion inferred f:removeKey(source, text(key)) from the generic pattern; this is the natural idiom for hiding internal fields |
DONE (same day, code-verified in plugins_e8.rs: map or list-of-maps, N key args, non-map list elements pass through, returns a copy) — syntax in the plugin table + a worked removeKey example in syntax.md; removeKey/listOfMap added to the KG grammar's f: examples + JSONPath wildcard note in minigraph-commands.json |
| 29 | Tut 8 | graph.math for_each[]/BEGIN/END is thinly specified — no worked example, loop-variable binding / iteration order / interaction with the "MAPPING-only rejected" rule unstated (the fetcher's for_each got full rules in #17/#18; the math skill's didn't) |
DONE (2026-07-19, engine-verified end-to-end): a probe fixture (rust-foreach.json) + permanent test math_for_each_blocks_and_iteration in graph_runtime.rs pinned every rule — pre/each/post BEGIN/END blocks (no BEGIN ⇒ whole list loops), strictly sequential in-order iteration inside ONE node execution, lockstep parallel arrays (equal length required), scalar entries bind once, unresolvable LHS removes the model key, a taken IF jump breaks the loop and skips the post-block, empty lists skip only the body. Probe also surfaced a new dialect truth: COMPUTE doubles didn't feed the then-whole-number-only f:add — resolved same day by increment 39 (numeric promotion, both ports): f:add now composes with COMPUTE results. New for_each section + JSON catalog for_each object + skills-reference + help graph-math.md (webapp bundle re-released, 124 tests green) |
| 30 | Tut 8 | island-scope crispness for graphs with no config nodes: the recipe said wire an island "if the graph has Dictionary/Provider (or data-entity) nodes" — for a pure-transformation graph, is a knowledge layer expected? (The tut-8 companion volunteered one with data-entity nodes documenting the domain) | DONE (maintainer ruling, 2026-07-19): island required whenever config/data-entity nodes exist; encouraged otherwise as ER documentation — wording landed in command-reference.md#island, ai-agent-guide.md recipe, minigraph-commands.json. Bonus context captured in syntax.md: listOfMap/JSONPath serve impedance matching between external (3rd-party) and internal API contracts |
| 31 | Tut 9 | EXECUTE result-landing was unspecified — "run another graph.math node inline" said nothing about where COMPUTE results land; they land on the invoking node ({invoker}.result.{var}), the module's namespace stays empty (the canonical walkthrough's own teaching point: "it just borrows the logic"). Cost the companion one self-corrected iteration |
DONE (same day, engine behavior proven live by the companion's own inspects) — EXECUTE semantics + a full reusable-module worked example (module + caller + island anchoring) in command-reference.md#math-statements; mirrored in minigraph-commands.json statement_syntax.EXECUTE + skills-reference.md |
| 32 | Tut 9 | node-type case contradiction: #create said "{name} and {type} are lowercase-and-hyphen" while the lexical table and every worked example Capitalize types (engine accepts them) |
DONE (same day) — #create bullet + minigraph-commands.json note corrected: name lowercase+hyphen; type a descriptive label, conventionally Capitalized |
| 33 | Tut 9 | island scope didn't name module/library nodes (examples showed only Dictionary/Provider/entities — a graph.math module under an island "worked but unstated"), and "never executed" contradicted the run log's Executed … with skill graph.island line |
DONE (same day) — island convention now covers off-path nodes: config, data-entity, and reusable Module (island -[module]-> module); phrasing corrected to "executes only to sink; traversal never continues through it" |
| 34 | Tut 10 | graph.extension's data contract was example-implied, not rule-stated — that bare input[] targets become the sub-graph's input.body.{key}, that the node's result.* is the sub-graph's output.body, and where extension={graph-id} resolves (deployed registry; drafts not addressable; missing-id failure mode) were all inferences from one example |
DONE (same day, engine-verified in extension.rs/graphs registry) — "The delegation contract" rules block in skills-reference.md#extension + expanded notes in minigraph-commands.json |
| 35 | Tut 10 | the node-name lexical rule wrongly excluded digits ("lowercase letters and hyphen only") — canonical fixtures themselves use fetcher-1/fetcher-2; engine-verified: no charset validation at create. Consequence: agents avoid natural names (e.g. one documenting tutorial-3) |
DONE (same day, + maintainer confirmation): digits (0-9) are legal — "lowercase letters, digits and hyphen" aligned across the lexical table, #create, invariants, pre-send checklist, and minigraph-commands.json. Hyphenated names are encouraged (communicative); the {node-name.key} substitution syntax in graph.math expressions is robust to hyphens (never parsed as a minus) — a briefly-added hyphen-avoidance style note was withdrawn by the maintainer |
| 36 | Tut 10 | skill-less End legality was only implied ("often graph.data.mapper") — used successfully in tut-9 and tut-10 when an upstream node already shaped output.body |
DONE (same day) — explicitly blessed in the node-types table |
| 37 | Tut 11 | whole-body * forwarding on graph.extension was unspecified (graph.task documents the input.body -> * merge; extension didn't say) |
DONE (same day, engine-verified: extension.rs stages named keys only — no * support) — stated in the delegation-contract block (skills-reference.md#extension) + minigraph-commands.json, incl. the flow:// case |
| 38 | Tut 11 | no discovery command for deployed flow/graph ids — agents rely on the brief for flow:///extension= targets; nothing like list flows / list graphs exists in the command grammar |
DONE (2026-07-20, increment 42, both ports): list graphs (compiled registry ∪ deployed folder, each with the root's purpose — living documentation) + list flows (Event Script flows), available on the console and both companion endpoints; documented in the grammar/catalog/AI guide/skills-reference/help list.md; Rust playground.rs + Java CompanionSyncTest coverage (70-test suite green, branch feat/discovery-commands pushed for upstream PR) |
| 39 | Tut 11 | export's reply references Described in /api/graph/model/{name}/{token} — an endpoint absent from the AI docs' endpoint table |
DONE (same day) — noted in command-reference.md#export |
| 40 | Tut 12 pre-flight | /sync ok heuristic false-negative: import graph from {deployed} succeeds but returns ok:false — is_error_line trips on the benign "Graph model not found in /tmp/… Found deployed graph model" fallback line (both ports share the heuristic) |
DONE (2026-07-19, engine fix, both ports): classification is now whole-output-aware — the not-found line is forgiven only when the same output also carries the fallback's success marker ("Found deployed graph model"); a genuine miss prints the not-found line alone and stays ok:false. Rust: first_error_line in rest.rs + both-direction test in graph_runtime.rs; Java: mirrored in PostCompanionCommandSync + CompanionSyncTest (66-test module suite green, local branch fix/companion-sync-ok-heuristic prepared for upstream PR). ai-agent-guide.md caveat replaced with the fixed semantics; minigraph-commands.json sync_envelope note added |
| 41 | Tut 12 | exception= semantics were one matrix line — jump-not-abort, output[] skipped on failure, no-handler ⇒ abort, handler wiring: all unstated |
DONE (same day, engine-verified in fetcher.rs) — new Failure routing section with the canonical bounded-retry pattern; fetcher gotchas + minigraph-commands.json |
| 42 | Tut 12 | RESET edge cases unstated: comma lists (RESET: a, b — canonical uses it), reset of a never-executed node (safe no-op), self-reset (allowed), and the loop guard (>10 visits/second aborts) |
DONE (same day, engine-verified in common.rs::reset_nodes + traveler/executor seen-marking + maintainer follow-up): self-reset is effective, not a no-op — the run-once mark is set before execution, so a self-reset survives (canonical tut-12 relies on it; live-probed). Nuance: a self-RESET also wipes the node's own in-flight state — place it before DELAY: (whose pending pause lives in node state); canonical order RESET → NEXT → DELAY is the safe one. Statement grammar + JSON catalog. Refined again (maintainer follow-up + live probe): the placement rule is RESET first among the action statements — it runs on every path (a taken IF jump skips later statements) — with the one exception that a check reading soon-wiped state (e.g. {fetcher.status}) must precede it: an IF on a wiped variable aborts the run (probe-verified). Tutorial-12 + the failure-routing pattern + all RESET guidance updated; reordered handler validated live (3 retries → recovery → success) |
| 43 | Tut 12 | whether statements after a taken IF jump run was unspecified |
DONE (same day, engine-verified in skills.rs::execute_statements): a taken node-jump ends the statement list; a next branch falls through — stated in the IF rules |
| 44 | Tut 12 | NEXT:/DELAY: presentation: listed beside properties in the matrix, never shown as written statement[] lines; NEXT's apply-at-end (last-wins) and DELAY's after-node timing unstated |
DONE (same day, engine-verified) — keyword block rewritten with written forms + timing semantics |
| 45 | Tut 12 | unresolvable mapping source behavior unstated (skip vs null) — the companion invented default-then-overlay defensively | DONE (same day, engine-verified): an unresolvable source skips the entry (target untouched); both default idioms documented (f:defaultValue, default-then-overlay) |
| 46 | Tut 12 | dedup scope vs retry ambiguity: "identical requests are deduplicated" left open whether a RESET re-execution would hit a cache and never re-call | DONE (same day, engine-verified in fetcher.rs): dedup is per graph instance and caches successful responses only — a failed call is never cached, so retries always re-call |
| 47 | Tut 13 | bare-result semantics for graph.task were shown, not stated — the explicit whole-result rule existed only for graph.extension; the companion inferred it by analogy |
DONE (same day) — the rule now stated in skills-reference.md#task, aligned with graph.extension |
| 48 | Tut 13 | the graph.task example was labeled "(Tutorial 13)" — a pointer outside the self-sufficient doc set |
DONE (same day) — relabeled |