agentopology 0.2.5 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +121 -7
- package/dist/analyzer/__tests__/analyzer.test.js +36 -9
- package/dist/analyzer/__tests__/analyzer.test.js.map +1 -1
- package/dist/analyzer/__tests__/observability-gap.test.js +40 -10
- package/dist/analyzer/__tests__/observability-gap.test.js.map +1 -1
- package/dist/analyzer/index.d.ts +2 -1
- package/dist/analyzer/index.d.ts.map +1 -1
- package/dist/analyzer/index.js +1 -1
- package/dist/analyzer/index.js.map +1 -1
- package/dist/bindings/__tests__/bindings.test.js +20 -5
- package/dist/bindings/__tests__/bindings.test.js.map +1 -1
- package/dist/bindings/__tests__/claude-workflow.test.js +12 -3
- package/dist/bindings/__tests__/claude-workflow.test.js.map +1 -1
- package/dist/bindings/__tests__/defaults-agreement.test.d.ts +16 -0
- package/dist/bindings/__tests__/defaults-agreement.test.d.ts.map +1 -0
- package/dist/bindings/__tests__/defaults-agreement.test.js +80 -0
- package/dist/bindings/__tests__/defaults-agreement.test.js.map +1 -0
- package/dist/bindings/__tests__/observability.test.js +8 -2
- package/dist/bindings/__tests__/observability.test.js.map +1 -1
- package/dist/bindings/__tests__/stub-generation.test.js +8 -2
- package/dist/bindings/__tests__/stub-generation.test.js.map +1 -1
- package/dist/bindings/claude-code.d.ts.map +1 -1
- package/dist/bindings/claude-code.js +102 -3
- package/dist/bindings/claude-code.js.map +1 -1
- package/dist/bindings/claude-workflow.d.ts.map +1 -1
- package/dist/bindings/claude-workflow.js +2 -1
- package/dist/bindings/claude-workflow.js.map +1 -1
- package/dist/bindings/codex.d.ts.map +1 -1
- package/dist/bindings/codex.js +6 -2
- package/dist/bindings/codex.js.map +1 -1
- package/dist/bindings/gemini-cli.d.ts.map +1 -1
- package/dist/bindings/gemini-cli.js +2 -1
- package/dist/bindings/gemini-cli.js.map +1 -1
- package/dist/bindings/kiro.d.ts.map +1 -1
- package/dist/bindings/kiro.js +2 -1
- package/dist/bindings/kiro.js.map +1 -1
- package/dist/bindings/openclaw.d.ts.map +1 -1
- package/dist/bindings/openclaw.js +2 -1
- package/dist/bindings/openclaw.js.map +1 -1
- package/dist/cli/__tests__/cli.test.d.ts +12 -0
- package/dist/cli/__tests__/cli.test.d.ts.map +1 -0
- package/dist/cli/__tests__/cli.test.js +171 -0
- package/dist/cli/__tests__/cli.test.js.map +1 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +344 -5
- package/dist/cli/index.js.map +1 -1
- package/dist/docs/content.d.ts.map +1 -1
- package/dist/docs/content.js +112 -0
- package/dist/docs/content.js.map +1 -1
- package/dist/exporters/ascii.d.ts +13 -0
- package/dist/exporters/ascii.d.ts.map +1 -0
- package/dist/exporters/ascii.js +26 -0
- package/dist/exporters/ascii.js.map +1 -0
- package/dist/exporters/brief.d.ts +14 -0
- package/dist/exporters/brief.d.ts.map +1 -0
- package/dist/exporters/brief.js +37 -0
- package/dist/exporters/brief.js.map +1 -0
- package/dist/exporters/index.d.ts +2 -0
- package/dist/exporters/index.d.ts.map +1 -1
- package/dist/exporters/index.js +6 -0
- package/dist/exporters/index.js.map +1 -1
- package/dist/exporters/markdown.d.ts.map +1 -1
- package/dist/exporters/markdown.js +2 -1
- package/dist/exporters/markdown.js.map +1 -1
- package/dist/exporters/mermaid.d.ts.map +1 -1
- package/dist/exporters/mermaid.js +2 -1
- package/dist/exporters/mermaid.js.map +1 -1
- package/dist/import/__tests__/import.test.js +8 -2
- package/dist/import/__tests__/import.test.js.map +1 -1
- package/dist/parser/__tests__/parser.test.js +1553 -207
- package/dist/parser/__tests__/parser.test.js.map +1 -1
- package/dist/parser/__tests__/spec-sync.test.d.ts +10 -0
- package/dist/parser/__tests__/spec-sync.test.d.ts.map +1 -0
- package/dist/parser/__tests__/spec-sync.test.js +60 -0
- package/dist/parser/__tests__/spec-sync.test.js.map +1 -0
- package/dist/parser/ast.d.ts +39 -0
- package/dist/parser/ast.d.ts.map +1 -1
- package/dist/parser/index.d.ts +50 -6
- package/dist/parser/index.d.ts.map +1 -1
- package/dist/parser/index.js +241 -30
- package/dist/parser/index.js.map +1 -1
- package/dist/parser/validator.d.ts +7 -1
- package/dist/parser/validator.d.ts.map +1 -1
- package/dist/parser/validator.js +143 -5
- package/dist/parser/validator.js.map +1 -1
- package/dist/plan/__tests__/brief.test.d.ts +2 -0
- package/dist/plan/__tests__/brief.test.d.ts.map +1 -0
- package/dist/plan/__tests__/brief.test.js +442 -0
- package/dist/plan/__tests__/brief.test.js.map +1 -0
- package/dist/plan/brief.d.ts +207 -0
- package/dist/plan/brief.d.ts.map +1 -0
- package/dist/plan/brief.js +342 -0
- package/dist/plan/brief.js.map +1 -0
- package/dist/plan/render.d.ts +20 -0
- package/dist/plan/render.d.ts.map +1 -0
- package/dist/plan/render.js +425 -0
- package/dist/plan/render.js.map +1 -0
- package/dist/render/__tests__/ascii.test.d.ts +2 -0
- package/dist/render/__tests__/ascii.test.d.ts.map +1 -0
- package/dist/render/__tests__/ascii.test.js +171 -0
- package/dist/render/__tests__/ascii.test.js.map +1 -0
- package/dist/render/ascii.d.ts +23 -0
- package/dist/render/ascii.d.ts.map +1 -0
- package/dist/render/ascii.js +178 -0
- package/dist/render/ascii.js.map +1 -0
- package/dist/resolve/__tests__/defaults.test.d.ts +2 -0
- package/dist/resolve/__tests__/defaults.test.d.ts.map +1 -0
- package/dist/resolve/__tests__/defaults.test.js +109 -0
- package/dist/resolve/__tests__/defaults.test.js.map +1 -0
- package/dist/resolve/__tests__/order.test.d.ts +2 -0
- package/dist/resolve/__tests__/order.test.d.ts.map +1 -0
- package/dist/resolve/__tests__/order.test.js +238 -0
- package/dist/resolve/__tests__/order.test.js.map +1 -0
- package/dist/resolve/__tests__/phase.test.d.ts +12 -0
- package/dist/resolve/__tests__/phase.test.d.ts.map +1 -0
- package/dist/resolve/__tests__/phase.test.js +81 -0
- package/dist/resolve/__tests__/phase.test.js.map +1 -0
- package/dist/resolve/defaults.d.ts +51 -0
- package/dist/resolve/defaults.d.ts.map +1 -0
- package/dist/resolve/defaults.js +109 -0
- package/dist/resolve/defaults.js.map +1 -0
- package/dist/resolve/order.d.ts +73 -0
- package/dist/resolve/order.d.ts.map +1 -0
- package/dist/resolve/order.js +208 -0
- package/dist/resolve/order.js.map +1 -0
- package/dist/resolve/phase.d.ts +57 -0
- package/dist/resolve/phase.d.ts.map +1 -0
- package/dist/resolve/phase.js +60 -0
- package/dist/resolve/phase.js.map +1 -0
- package/dist/scaffold/__tests__/scaffold.test.js +8 -2
- package/dist/scaffold/__tests__/scaffold.test.js.map +1 -1
- package/dist/scaffold/__tests__/settings-merge.test.js +95 -0
- package/dist/scaffold/__tests__/settings-merge.test.js.map +1 -1
- package/dist/scaffold/incremental.d.ts.map +1 -1
- package/dist/scaffold/incremental.js +13 -2
- package/dist/scaffold/incremental.js.map +1 -1
- package/dist/visualizer/__tests__/brain.test.d.ts +2 -0
- package/dist/visualizer/__tests__/brain.test.d.ts.map +1 -0
- package/dist/visualizer/__tests__/brain.test.js +138 -0
- package/dist/visualizer/__tests__/brain.test.js.map +1 -0
- package/dist/visualizer/__tests__/topology-brain-link.test.d.ts +2 -0
- package/dist/visualizer/__tests__/topology-brain-link.test.d.ts.map +1 -0
- package/dist/visualizer/__tests__/topology-brain-link.test.js +56 -0
- package/dist/visualizer/__tests__/topology-brain-link.test.js.map +1 -0
- package/dist/visualizer/brain.d.ts +97 -0
- package/dist/visualizer/brain.d.ts.map +1 -0
- package/dist/visualizer/brain.js +473 -0
- package/dist/visualizer/brain.js.map +1 -0
- package/dist/visualizer/index.d.ts +19 -1
- package/dist/visualizer/index.d.ts.map +1 -1
- package/dist/visualizer/index.js +26 -11
- package/dist/visualizer/index.js.map +1 -1
- package/examples/code-review.at +32 -4
- package/examples/company-brain-team.at +241 -0
- package/examples/company-brain.at +104 -0
- package/examples/data-processing.at +33 -4
- package/examples/logos/gmail.svg +1 -0
- package/examples/logos/slack.svg +1 -0
- package/package.json +1 -1
- package/spec/grammar.md +55 -3
- package/spec/validation.md +103 -35
package/dist/parser/index.js
CHANGED
|
@@ -16,6 +16,78 @@ import { stripComments, extractBlock, extractAllBlocks, parseKV, parseList, pars
|
|
|
16
16
|
// ---------------------------------------------------------------------------
|
|
17
17
|
// Helpers
|
|
18
18
|
// ---------------------------------------------------------------------------
|
|
19
|
+
/**
|
|
20
|
+
* V90: detect a field value that swallowed the next field.
|
|
21
|
+
*
|
|
22
|
+
* `parseFields` is line-based, and the grammar never says so. Writing two
|
|
23
|
+
* fields on one line — `agent a { model: sonnet retry: 3 }` — makes the first
|
|
24
|
+
* key take the whole rest of the line as its value (`"sonnet retry: 3"`), and
|
|
25
|
+
* nothing catches it. The value is then silently wrong: a model name that does
|
|
26
|
+
* not exist, a version string carrying a description, an action `kind` that
|
|
27
|
+
* fails its enum for a confusing reason.
|
|
28
|
+
*
|
|
29
|
+
* Detection: an UNQUOTED value containing whitespace followed by
|
|
30
|
+
* `identifier:` is almost certainly a swallowed field. Quoted values are
|
|
31
|
+
* exempt (a description may legitimately contain a colon), and a leading
|
|
32
|
+
* scheme like `https://` is not matched because it has no preceding
|
|
33
|
+
* whitespace.
|
|
34
|
+
*/
|
|
35
|
+
function detectSwallowedFields(fields, nodeId, out) {
|
|
36
|
+
for (const [key, raw] of Object.entries(fields)) {
|
|
37
|
+
if (typeof raw !== "string")
|
|
38
|
+
continue;
|
|
39
|
+
const value = raw.trim();
|
|
40
|
+
// Free-prose fields whose quotes have already been stripped are exempt: a
|
|
41
|
+
// colon inside a description is ordinary English ("Demo: agent pipeline"),
|
|
42
|
+
// and once the quotes are gone there is no way to tell prose from a
|
|
43
|
+
// swallow. Where quotes SURVIVE parsing — every block except `meta` — the
|
|
44
|
+
// check below still catches `description: "k" model: sonnet`.
|
|
45
|
+
const PROSE_FIELDS = new Set(["description", "role", "generates", "termination"]);
|
|
46
|
+
if (PROSE_FIELDS.has(key) && !value.startsWith('"'))
|
|
47
|
+
continue;
|
|
48
|
+
// A balanced brace block is a NESTED OBJECT, not a swallow. The single-line
|
|
49
|
+
// form is spec-legal and parses correctly — `spec/grammar.md:117` gives
|
|
50
|
+
// `outputs: { depth: 1 | 2 | 3 }` verbatim. Flagging it rejected valid
|
|
51
|
+
// topologies, which is worse than missing one.
|
|
52
|
+
if (value.startsWith("{") && value.endsWith("}"))
|
|
53
|
+
continue;
|
|
54
|
+
// Same for a list.
|
|
55
|
+
if (value.startsWith("[") && value.endsWith("]"))
|
|
56
|
+
continue;
|
|
57
|
+
// A WELL-FORMED quoted string is exempt — a description may contain a
|
|
58
|
+
// colon. `"1.0.0" description: "x"` is NOT well-formed: it is one quoted
|
|
59
|
+
// token followed by more content, i.e. a swallowed field.
|
|
60
|
+
if (/^"(?:[^"\\]|\\.)*"$/.test(value))
|
|
61
|
+
continue;
|
|
62
|
+
const search = value.startsWith('"')
|
|
63
|
+
? value.replace(/^"(?:[^"\\]|\\.)*"/, "")
|
|
64
|
+
: value;
|
|
65
|
+
const m = /\s([a-zA-Z][a-zA-Z0-9_-]*):/.exec(search.startsWith(" ") ? search : " " + search);
|
|
66
|
+
if (!m)
|
|
67
|
+
continue;
|
|
68
|
+
out.push({
|
|
69
|
+
rule: "V90",
|
|
70
|
+
level: "error",
|
|
71
|
+
message: `"${nodeId}" field \`${key}\` has the value \`${value}\`, which looks like it swallowed the ` +
|
|
72
|
+
`field \`${m[1]}\` — fields must be one per line`,
|
|
73
|
+
node: nodeId,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Placeholders the parser injects for REQUIRED fields that were not declared.
|
|
79
|
+
*
|
|
80
|
+
* The parser cannot leave them undefined without changing non-optional AST
|
|
81
|
+
* types, so it substitutes a sentinel — and that sentinel then satisfied every
|
|
82
|
+
* "is it present?" check downstream. V91 recognises them and reports the field
|
|
83
|
+
* as missing, which is what the spec says it is.
|
|
84
|
+
*/
|
|
85
|
+
export const MISSING = {
|
|
86
|
+
/** `orchestrator.model` — spec/grammar.md, orchestrator field table. */
|
|
87
|
+
model: "unknown",
|
|
88
|
+
/** `meta.version` — spec/grammar.md:282, Required: yes. */
|
|
89
|
+
version: "0.0.0",
|
|
90
|
+
};
|
|
19
91
|
/** Convert a kebab-case or snake_case identifier to a Title Case label. */
|
|
20
92
|
function toLabel(id) {
|
|
21
93
|
return id
|
|
@@ -203,8 +275,11 @@ export function parseMeta(body) {
|
|
|
203
275
|
/**
|
|
204
276
|
* Parse the `orchestrator { ... }` block into an {@link OrchestratorNode}.
|
|
205
277
|
*/
|
|
206
|
-
export function parseOrchestrator(body) {
|
|
278
|
+
export function parseOrchestrator(body, _blockFieldMisuseWarnings) {
|
|
207
279
|
const fields = parseFields(body);
|
|
280
|
+
if (_blockFieldMisuseWarnings) {
|
|
281
|
+
detectSwallowedFields(fields, "orchestrator", _blockFieldMisuseWarnings);
|
|
282
|
+
}
|
|
208
283
|
const handles = parseMultilineList(body, "handles");
|
|
209
284
|
const outputs = parseOutputsBlock(body);
|
|
210
285
|
const node = {
|
|
@@ -247,8 +322,11 @@ export function parseRoles(body) {
|
|
|
247
322
|
/**
|
|
248
323
|
* Parse an `action <id> { ... }` block into an {@link ActionNode}.
|
|
249
324
|
*/
|
|
250
|
-
export function parseAction(id, body) {
|
|
325
|
+
export function parseAction(id, body, _blockFieldMisuseWarnings) {
|
|
251
326
|
const fields = parseFields(body);
|
|
327
|
+
if (_blockFieldMisuseWarnings) {
|
|
328
|
+
detectSwallowedFields(fields, id, _blockFieldMisuseWarnings);
|
|
329
|
+
}
|
|
252
330
|
const commands = parseMultilineList(body, "commands");
|
|
253
331
|
const node = {
|
|
254
332
|
id,
|
|
@@ -279,7 +357,7 @@ export function parseAction(id, body) {
|
|
|
279
357
|
* @param roles - Role descriptions from the `roles` block, used to attach a
|
|
280
358
|
* role description to matching agents.
|
|
281
359
|
*/
|
|
282
|
-
export function parseAgent(id, body, roles) {
|
|
360
|
+
export function parseAgent(id, body, roles, _blockFieldMisuseWarnings) {
|
|
283
361
|
const fields = parseFields(body);
|
|
284
362
|
const tools = parseMultilineList(body, "tools");
|
|
285
363
|
const skills = parseMultilineList(body, "skills");
|
|
@@ -298,7 +376,24 @@ export function parseAgent(id, body, roles) {
|
|
|
298
376
|
node.model = fields.model;
|
|
299
377
|
if (fields.permissions)
|
|
300
378
|
node.permissions = fields.permissions;
|
|
301
|
-
// Extract prompt from prompt {} block (not KV pair)
|
|
379
|
+
// Extract prompt from prompt {} block (not KV pair).
|
|
380
|
+
//
|
|
381
|
+
// V89: `agent.prompt` is a BLOCK per spec/grammar.md:350. The string form
|
|
382
|
+
// (`prompt: "path.md"`) belongs to `skill`, a different node type
|
|
383
|
+
// (spec/grammar.md:1377). A KV `prompt` on an agent is silently unparseable,
|
|
384
|
+
// so flag it rather than dropping the author's instructions on the floor.
|
|
385
|
+
if (fields.prompt) {
|
|
386
|
+
_blockFieldMisuseWarnings?.push({
|
|
387
|
+
rule: "V89",
|
|
388
|
+
level: "error",
|
|
389
|
+
message: `agent "${id}" uses \`prompt:\` as a key-value field, but agent prompts must be a \`prompt { }\` block — ` +
|
|
390
|
+
`this value is ignored and the agent will have no instructions`,
|
|
391
|
+
node: id,
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
if (_blockFieldMisuseWarnings) {
|
|
395
|
+
detectSwallowedFields(fields, id, _blockFieldMisuseWarnings);
|
|
396
|
+
}
|
|
302
397
|
const promptBlock = extractBlock(body, "prompt");
|
|
303
398
|
if (promptBlock) {
|
|
304
399
|
node.prompt = dedentBlock(promptBlock.body);
|
|
@@ -407,6 +502,24 @@ export function parseAgent(id, body, roles) {
|
|
|
407
502
|
}
|
|
408
503
|
if (fields.retrieval)
|
|
409
504
|
node.retrieval = fields.retrieval;
|
|
505
|
+
// Custody: `custodian-of: [store, ...]` with optional `{ does: [verb, ...] }`.
|
|
506
|
+
// The store-list appears before an optional brace block. Match the list first,
|
|
507
|
+
// then look inside the block (if any) for `does`.
|
|
508
|
+
const custodyMatch = /custodian-of\s*:\s*\[([^\]]*)\]/.exec(body);
|
|
509
|
+
if (custodyMatch) {
|
|
510
|
+
const stores = parseList(custodyMatch[1]);
|
|
511
|
+
if (stores.length)
|
|
512
|
+
node.custodianOf = stores;
|
|
513
|
+
const custodyBlock = extractBlock(body, "custodian-of");
|
|
514
|
+
if (custodyBlock) {
|
|
515
|
+
const does = parseMultilineList(custodyBlock.body, "does");
|
|
516
|
+
const doesInline = parseFields(custodyBlock.body).does;
|
|
517
|
+
if (does.length)
|
|
518
|
+
node.custodianDoes = does;
|
|
519
|
+
else if (doesInline)
|
|
520
|
+
node.custodianDoes = parseList(doesInline);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
410
523
|
if (outputs)
|
|
411
524
|
node.outputs = outputs;
|
|
412
525
|
// Parse scale sub-block
|
|
@@ -537,8 +650,11 @@ export function parseAgent(id, body, roles) {
|
|
|
537
650
|
/**
|
|
538
651
|
* Parse a `gate <id> { ... }` block into a {@link GateNode}.
|
|
539
652
|
*/
|
|
540
|
-
export function parseGate(id, body) {
|
|
653
|
+
export function parseGate(id, body, _blockFieldMisuseWarnings) {
|
|
541
654
|
const fields = parseFields(body);
|
|
655
|
+
if (_blockFieldMisuseWarnings) {
|
|
656
|
+
detectSwallowedFields(fields, id, _blockFieldMisuseWarnings);
|
|
657
|
+
}
|
|
542
658
|
const checks = parseMultilineList(body, "checks");
|
|
543
659
|
const node = {
|
|
544
660
|
id,
|
|
@@ -569,8 +685,11 @@ export function parseGate(id, body) {
|
|
|
569
685
|
/**
|
|
570
686
|
* Parse a `human <id> { ... }` block into a {@link HumanNode}.
|
|
571
687
|
*/
|
|
572
|
-
export function parseHuman(id, body) {
|
|
688
|
+
export function parseHuman(id, body, _blockFieldMisuseWarnings) {
|
|
573
689
|
const fields = parseFields(body);
|
|
690
|
+
if (_blockFieldMisuseWarnings) {
|
|
691
|
+
detectSwallowedFields(fields, id, _blockFieldMisuseWarnings);
|
|
692
|
+
}
|
|
574
693
|
const node = {
|
|
575
694
|
id,
|
|
576
695
|
type: "human",
|
|
@@ -587,8 +706,11 @@ export function parseHuman(id, body) {
|
|
|
587
706
|
/**
|
|
588
707
|
* Parse a `group <id> { ... }` block into a {@link GroupNode}.
|
|
589
708
|
*/
|
|
590
|
-
export function parseGroup(id, body) {
|
|
709
|
+
export function parseGroup(id, body, _blockFieldMisuseWarnings) {
|
|
591
710
|
const fields = parseFields(body);
|
|
711
|
+
if (_blockFieldMisuseWarnings) {
|
|
712
|
+
detectSwallowedFields(fields, id, _blockFieldMisuseWarnings);
|
|
713
|
+
}
|
|
592
714
|
const members = parseList(fields.members ?? "");
|
|
593
715
|
const node = {
|
|
594
716
|
id,
|
|
@@ -633,6 +755,24 @@ export function parseFlow(body, _edgeAttributeErrors) {
|
|
|
633
755
|
// Match annotation at end of line: [when ..., max N, per agent-id, tolerance: N, race, wait 30s, reflection]
|
|
634
756
|
// Must start with "when", "max", "per", "tolerance", "race", "wait", "join", "weight", or "reflection" to avoid matching fan-out lists.
|
|
635
757
|
const bracketMatch = trimmed.match(/\[(when\s+.+?|max\s+\d+.*?|per\s+\S+.*?|tolerance\s*:.+?|race.*?|wait\s+\S+.*?|join\s+\S+.*?|weight\s+\S+.*?|reflection.*?)\]\s*$/);
|
|
758
|
+
// V92: two bracket groups on one edge. The grammar defines ONE annotation
|
|
759
|
+
// with comma-separated attributes (`[when ..., max N]`). Written as
|
|
760
|
+
// `[when ...] [max N]`, the matcher below spans from the first `[` to the
|
|
761
|
+
// last `]` and produces a corrupt condition — literally
|
|
762
|
+
// `a.verdict == fail] [max 3` — with no error at all.
|
|
763
|
+
// Count only ANNOTATION brackets. A fan-out list is also bracketed —
|
|
764
|
+
// `analyzer -> [reporter, alerter] [when ...]` is legal — so testing for
|
|
765
|
+
// `] [` alone flagged valid topologies.
|
|
766
|
+
const ATTR = /\[\s*(?:when|max|per|tolerance|race|wait|join|weight|reflection)\b/g;
|
|
767
|
+
const annotationBrackets = (trimmed.match(ATTR) ?? []).length;
|
|
768
|
+
if (annotationBrackets > 1 && _edgeAttributeErrors) {
|
|
769
|
+
_edgeAttributeErrors.push({
|
|
770
|
+
rule: "V92",
|
|
771
|
+
level: "error",
|
|
772
|
+
message: "Edge has two bracket groups — attributes go in ONE bracket, " +
|
|
773
|
+
`comma-separated: [when ..., max N] (line: ${trimmed})`,
|
|
774
|
+
});
|
|
775
|
+
}
|
|
636
776
|
if (bracketMatch) {
|
|
637
777
|
flowPart = trimmed.slice(0, bracketMatch.index).trim();
|
|
638
778
|
const annotation = bracketMatch[1];
|
|
@@ -892,10 +1032,13 @@ function parseScoringConfig(body) {
|
|
|
892
1032
|
*/
|
|
893
1033
|
export function parseStore(id, body) {
|
|
894
1034
|
const fields = parseFields(body);
|
|
1035
|
+
// `brain` stores are file-native (Obsidian-format markdown) — their backend
|
|
1036
|
+
// is always `files`, never a database. Every other type defaults to lancedb.
|
|
1037
|
+
const type = fields.type ?? "semantic";
|
|
895
1038
|
const store = {
|
|
896
1039
|
id,
|
|
897
|
-
type
|
|
898
|
-
backend: fields.backend ?? "lancedb",
|
|
1040
|
+
type,
|
|
1041
|
+
backend: fields.backend ?? (type === "brain" ? "files" : "lancedb"),
|
|
899
1042
|
};
|
|
900
1043
|
if (fields.description)
|
|
901
1044
|
store.description = unquote(fields.description);
|
|
@@ -907,6 +1050,8 @@ export function parseStore(id, body) {
|
|
|
907
1050
|
store.path = unquote(fields.path);
|
|
908
1051
|
if (fields.extraction)
|
|
909
1052
|
store.extraction = fields.extraction;
|
|
1053
|
+
if (fields.format)
|
|
1054
|
+
store.format = fields.format;
|
|
910
1055
|
// Connection: may use `secret "uri"` modifier (same pattern as parseCheckpoint)
|
|
911
1056
|
if (fields.connection) {
|
|
912
1057
|
const rawConn = fields.connection.trim();
|
|
@@ -933,6 +1078,39 @@ export function parseStore(id, body) {
|
|
|
933
1078
|
const lifecycleBlock = extractBlock(body, "lifecycle");
|
|
934
1079
|
if (lifecycleBlock)
|
|
935
1080
|
store.lifecycle = parseLifecycleConfig(lifecycleBlock.body);
|
|
1081
|
+
// Presentation-only `sources { gmail { color icon } slack { ... } }` block.
|
|
1082
|
+
// Maps a note's `source:` frontmatter to a graph color/icon. Cosmetic — the
|
|
1083
|
+
// visualizer reads it; bindings ignore it entirely.
|
|
1084
|
+
const sourcesBlock = extractBlock(body, "sources");
|
|
1085
|
+
if (sourcesBlock) {
|
|
1086
|
+
const sources = {};
|
|
1087
|
+
const re = /(?:^|\n)\s*([a-zA-Z][a-zA-Z0-9_-]*)\s*\{/gm;
|
|
1088
|
+
let sm;
|
|
1089
|
+
while ((sm = re.exec(sourcesBlock.body)) !== null) {
|
|
1090
|
+
const name = sm[1];
|
|
1091
|
+
const startIdx = sm.index + sm[0].length;
|
|
1092
|
+
let depth = 1, i = startIdx;
|
|
1093
|
+
while (i < sourcesBlock.body.length && depth > 0) {
|
|
1094
|
+
if (sourcesBlock.body[i] === "{")
|
|
1095
|
+
depth++;
|
|
1096
|
+
else if (sourcesBlock.body[i] === "}")
|
|
1097
|
+
depth--;
|
|
1098
|
+
i++;
|
|
1099
|
+
}
|
|
1100
|
+
if (depth === 0) {
|
|
1101
|
+
const inner = parseFields(sourcesBlock.body.slice(startIdx, i - 1));
|
|
1102
|
+
const style = {};
|
|
1103
|
+
if (inner.color)
|
|
1104
|
+
style.color = unquote(inner.color);
|
|
1105
|
+
if (inner.icon)
|
|
1106
|
+
style.icon = unquote(inner.icon);
|
|
1107
|
+
if (style.color || style.icon)
|
|
1108
|
+
sources[name] = style;
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
if (Object.keys(sources).length > 0)
|
|
1112
|
+
store.sources = sources;
|
|
1113
|
+
}
|
|
936
1114
|
// Backend-config passthrough bag
|
|
937
1115
|
const backendConfigBlock = extractBlock(body, "backend-config");
|
|
938
1116
|
if (backendConfigBlock) {
|
|
@@ -1006,10 +1184,7 @@ export function parseMemory(body, _unknownSubBlockWarnings) {
|
|
|
1006
1184
|
];
|
|
1007
1185
|
const knownSubsSet = new Set([
|
|
1008
1186
|
...knownSubs,
|
|
1009
|
-
"store", "retrieval",
|
|
1010
|
-
// Sub-blocks within store {} and retrieval {} (not top-level memory subs,
|
|
1011
|
-
// but the V24 scanner sees them because it doesn't track depth)
|
|
1012
|
-
"embedding", "index", "ingestion", "search", "lifecycle", "backend-config", "scoring",
|
|
1187
|
+
"store", "retrieval", // the two typed top-level blocks
|
|
1013
1188
|
]);
|
|
1014
1189
|
for (const name of knownSubs) {
|
|
1015
1190
|
const block = extractBlock(body, name);
|
|
@@ -1042,19 +1217,47 @@ export function parseMemory(body, _unknownSubBlockWarnings) {
|
|
|
1042
1217
|
continue;
|
|
1043
1218
|
retrievals.push(parseRetrieval(block.id, block.body));
|
|
1044
1219
|
}
|
|
1045
|
-
// Detect unknown sub-blocks
|
|
1220
|
+
// Detect unknown sub-blocks. Only consider TOP-LEVEL blocks of the memory
|
|
1221
|
+
// body — names nested inside a `store {}` or `retrieval {}` (e.g. a brain
|
|
1222
|
+
// store's `sources { gmail {} }`) are that block's own concern, not memory
|
|
1223
|
+
// sub-blocks. Walk the body tracking brace depth so we only flag depth-0 names.
|
|
1046
1224
|
if (_unknownSubBlockWarnings) {
|
|
1047
|
-
|
|
1048
|
-
let
|
|
1049
|
-
while (
|
|
1050
|
-
const
|
|
1051
|
-
if (
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1225
|
+
let depth = 0;
|
|
1226
|
+
let i = 0;
|
|
1227
|
+
while (i < body.length) {
|
|
1228
|
+
const ch = body[i];
|
|
1229
|
+
if (ch === "}") {
|
|
1230
|
+
depth = Math.max(0, depth - 1);
|
|
1231
|
+
i++;
|
|
1232
|
+
continue;
|
|
1233
|
+
}
|
|
1234
|
+
if (ch === "{") {
|
|
1235
|
+
depth++;
|
|
1236
|
+
i++;
|
|
1237
|
+
continue;
|
|
1057
1238
|
}
|
|
1239
|
+
// At top level, match a block header before `{`. Handles both the bare
|
|
1240
|
+
// form (`workspace {`) and the typed-block form (`store brain {`,
|
|
1241
|
+
// `retrieval smart {`) — in the latter the FIRST token is the keyword.
|
|
1242
|
+
if (depth === 0) {
|
|
1243
|
+
const rest = body.slice(i);
|
|
1244
|
+
const m = /^([a-zA-Z][a-zA-Z0-9_-]*)(?:\s+[a-zA-Z][a-zA-Z0-9_-]*)?\s*\{/.exec(rest);
|
|
1245
|
+
if (m && (i === 0 || /\s/.test(body[i - 1]))) {
|
|
1246
|
+
const name = m[1]; // the leading keyword (store / retrieval / workspace / …)
|
|
1247
|
+
if (!knownSubsSet.has(name)) {
|
|
1248
|
+
_unknownSubBlockWarnings.push({
|
|
1249
|
+
rule: "V24",
|
|
1250
|
+
level: "warning",
|
|
1251
|
+
message: `Unknown sub-block "${name}" in memory block — known sub-blocks are: ${knownSubs.join(", ")}`,
|
|
1252
|
+
});
|
|
1253
|
+
}
|
|
1254
|
+
// Advance into the block (past the `{`) so its body is depth-counted.
|
|
1255
|
+
i += m[0].length;
|
|
1256
|
+
depth++;
|
|
1257
|
+
continue;
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
i++;
|
|
1058
1261
|
}
|
|
1059
1262
|
}
|
|
1060
1263
|
return { memory, stores, retrievals };
|
|
@@ -1970,7 +2173,11 @@ export function parse(source) {
|
|
|
1970
2173
|
const topBody = stripComments(topoBlock.body);
|
|
1971
2174
|
// --- Meta ---
|
|
1972
2175
|
const metaBlock = extractBlock(topBody, "meta");
|
|
2176
|
+
// V89/V90: collected while parsing blocks — a field written in a form the
|
|
2177
|
+
// grammar does not define. Attached to the AST below.
|
|
2178
|
+
const blockFieldMisuseWarnings = [];
|
|
1973
2179
|
const metaFields = metaBlock ? parseMeta(metaBlock.body) : {};
|
|
2180
|
+
detectSwallowedFields(metaFields, "<meta>", blockFieldMisuseWarnings);
|
|
1974
2181
|
const topology = {
|
|
1975
2182
|
name: header.name,
|
|
1976
2183
|
version: metaFields.version ?? "0.0.0",
|
|
@@ -1991,20 +2198,20 @@ export function parse(source) {
|
|
|
1991
2198
|
// Orchestrator
|
|
1992
2199
|
const orchBlock = extractBlock(topBody, "orchestrator");
|
|
1993
2200
|
if (orchBlock) {
|
|
1994
|
-
nodes.push(parseOrchestrator(orchBlock.body));
|
|
2201
|
+
nodes.push(parseOrchestrator(orchBlock.body, blockFieldMisuseWarnings));
|
|
1995
2202
|
}
|
|
1996
2203
|
// Actions
|
|
1997
2204
|
const actionBlocks = extractAllBlocks(topBody, "action");
|
|
1998
2205
|
for (const block of actionBlocks) {
|
|
1999
2206
|
if (block.id) {
|
|
2000
|
-
nodes.push(parseAction(block.id, block.body));
|
|
2207
|
+
nodes.push(parseAction(block.id, block.body, blockFieldMisuseWarnings));
|
|
2001
2208
|
}
|
|
2002
2209
|
}
|
|
2003
2210
|
// Agents
|
|
2004
2211
|
const agentBlocks = extractAllBlocks(topBody, "agent");
|
|
2005
2212
|
for (const block of agentBlocks) {
|
|
2006
2213
|
if (block.id) {
|
|
2007
|
-
nodes.push(parseAgent(block.id, block.body, roles));
|
|
2214
|
+
nodes.push(parseAgent(block.id, block.body, roles, blockFieldMisuseWarnings));
|
|
2008
2215
|
}
|
|
2009
2216
|
}
|
|
2010
2217
|
// Gates
|
|
@@ -2013,7 +2220,7 @@ export function parse(source) {
|
|
|
2013
2220
|
const gateBlocks = extractAllBlocks(gatesBlock.body, "gate");
|
|
2014
2221
|
for (const block of gateBlocks) {
|
|
2015
2222
|
if (block.id) {
|
|
2016
|
-
nodes.push(parseGate(block.id, block.body));
|
|
2223
|
+
nodes.push(parseGate(block.id, block.body, blockFieldMisuseWarnings));
|
|
2017
2224
|
}
|
|
2018
2225
|
}
|
|
2019
2226
|
}
|
|
@@ -2021,14 +2228,14 @@ export function parse(source) {
|
|
|
2021
2228
|
const humanBlocks = extractAllBlocks(topBody, "human");
|
|
2022
2229
|
for (const block of humanBlocks) {
|
|
2023
2230
|
if (block.id) {
|
|
2024
|
-
nodes.push(parseHuman(block.id, block.body));
|
|
2231
|
+
nodes.push(parseHuman(block.id, block.body, blockFieldMisuseWarnings));
|
|
2025
2232
|
}
|
|
2026
2233
|
}
|
|
2027
2234
|
// Group chat nodes
|
|
2028
2235
|
const groupBlocks = extractAllBlocks(topBody, "group");
|
|
2029
2236
|
for (const block of groupBlocks) {
|
|
2030
2237
|
if (block.id) {
|
|
2031
|
-
nodes.push(parseGroup(block.id, block.body));
|
|
2238
|
+
nodes.push(parseGroup(block.id, block.body, blockFieldMisuseWarnings));
|
|
2032
2239
|
}
|
|
2033
2240
|
}
|
|
2034
2241
|
// --- Skills ---
|
|
@@ -2209,6 +2416,10 @@ export function parse(source) {
|
|
|
2209
2416
|
if (unknownMemorySubBlockWarnings.length > 0) {
|
|
2210
2417
|
ast._unknownMemorySubBlockWarnings = unknownMemorySubBlockWarnings;
|
|
2211
2418
|
}
|
|
2419
|
+
// Attach V89 block-vs-KV field misuse errors for the validator to consume.
|
|
2420
|
+
if (blockFieldMisuseWarnings.length > 0) {
|
|
2421
|
+
ast._blockFieldMisuseWarnings = blockFieldMisuseWarnings;
|
|
2422
|
+
}
|
|
2212
2423
|
// Attach source map for line number tracking in validation results.
|
|
2213
2424
|
if (Object.keys(sourceMap).length > 0) {
|
|
2214
2425
|
ast._sourceMap = sourceMap;
|