@starci/skills 1.1.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/INDEX.md +74 -0
- package/INDEX.vi.md +75 -0
- package/README.md +44 -0
- package/README.vi.md +43 -0
- package/SKILL.md +135 -0
- package/SKILL.vi.md +128 -0
- package/alias/INDEX.md +104 -0
- package/alias/INDEX.vi.md +104 -0
- package/alias/alias.json +297 -0
- package/bin/starci-skills.mjs +219 -0
- package/knowledge/INDEX.md +22 -0
- package/knowledge/INDEX.vi.md +22 -0
- package/knowledge/grammars/starci/DNA.md +146 -0
- package/knowledge/grammars/starci/DNA.vi.md +146 -0
- package/knowledge/grammars/starci/INDEX.md +25 -0
- package/knowledge/grammars/starci/INDEX.vi.md +25 -0
- package/knowledge/grammars/starci/family.md +50 -0
- package/knowledge/grammars/starci/family.vi.md +50 -0
- package/knowledge/grammars/starci/idioms.md +120 -0
- package/knowledge/grammars/starci/idioms.vi.md +121 -0
- package/knowledge/grammars/starci/playbook.md +36 -0
- package/knowledge/grammars/starci/playbook.vi.md +35 -0
- package/knowledge/patterns/be/INDEX.md +29 -0
- package/knowledge/patterns/be/INDEX.vi.md +29 -0
- package/knowledge/patterns/be/comment.md +80 -0
- package/knowledge/patterns/be/comment.vi.md +80 -0
- package/knowledge/patterns/be/error.md +86 -0
- package/knowledge/patterns/be/error.vi.md +86 -0
- package/knowledge/patterns/be/folder.md +87 -0
- package/knowledge/patterns/be/folder.vi.md +86 -0
- package/knowledge/patterns/be/function.md +80 -0
- package/knowledge/patterns/be/function.vi.md +80 -0
- package/knowledge/patterns/be/imports.md +79 -0
- package/knowledge/patterns/be/imports.vi.md +79 -0
- package/knowledge/patterns/be/naming.md +87 -0
- package/knowledge/patterns/be/naming.vi.md +87 -0
- package/knowledge/patterns/be/test.md +79 -0
- package/knowledge/patterns/be/test.vi.md +79 -0
- package/knowledge/patterns/be/typing.md +73 -0
- package/knowledge/patterns/be/typing.vi.md +73 -0
- package/knowledge/patterns/fe/INDEX.md +29 -0
- package/knowledge/patterns/fe/INDEX.vi.md +29 -0
- package/knowledge/patterns/fe/comment.md +70 -0
- package/knowledge/patterns/fe/comment.vi.md +70 -0
- package/knowledge/patterns/fe/error.md +61 -0
- package/knowledge/patterns/fe/error.vi.md +61 -0
- package/knowledge/patterns/fe/folder.md +98 -0
- package/knowledge/patterns/fe/folder.vi.md +95 -0
- package/knowledge/patterns/fe/function.md +70 -0
- package/knowledge/patterns/fe/function.vi.md +70 -0
- package/knowledge/patterns/fe/imports.md +87 -0
- package/knowledge/patterns/fe/imports.vi.md +87 -0
- package/knowledge/patterns/fe/naming.md +77 -0
- package/knowledge/patterns/fe/naming.vi.md +77 -0
- package/knowledge/patterns/fe/test.md +73 -0
- package/knowledge/patterns/fe/test.vi.md +73 -0
- package/knowledge/patterns/fe/typing.md +67 -0
- package/knowledge/patterns/fe/typing.vi.md +67 -0
- package/knowledge/ui/INDEX.md +101 -0
- package/knowledge/ui/INDEX.vi.md +101 -0
- package/knowledge/ui/composition/INDEX.md +60 -0
- package/knowledge/ui/composition/INDEX.vi.md +63 -0
- package/knowledge/ui/composition/accent.md +73 -0
- package/knowledge/ui/composition/accent.vi.md +74 -0
- package/knowledge/ui/composition/action.md +55 -0
- package/knowledge/ui/composition/action.vi.md +56 -0
- package/knowledge/ui/composition/coverage.md +37 -0
- package/knowledge/ui/composition/coverage.vi.md +37 -0
- package/knowledge/ui/composition/cta.md +79 -0
- package/knowledge/ui/composition/cta.vi.md +79 -0
- package/knowledge/ui/composition/feedback.md +55 -0
- package/knowledge/ui/composition/feedback.vi.md +56 -0
- package/knowledge/ui/composition/hierarchy.md +83 -0
- package/knowledge/ui/composition/hierarchy.vi.md +82 -0
- package/knowledge/ui/composition/layout.md +91 -0
- package/knowledge/ui/composition/layout.vi.md +93 -0
- package/knowledge/ui/composition/responsive.md +67 -0
- package/knowledge/ui/composition/responsive.vi.md +67 -0
- package/knowledge/ui/composition/state.md +105 -0
- package/knowledge/ui/composition/state.vi.md +106 -0
- package/knowledge/ui/presentation/INDEX.md +94 -0
- package/knowledge/ui/presentation/INDEX.vi.md +69 -0
- package/knowledge/ui/presentation/boundary.md +170 -0
- package/knowledge/ui/presentation/boundary.vi.md +169 -0
- package/knowledge/ui/presentation/font.md +155 -0
- package/knowledge/ui/presentation/font.vi.md +156 -0
- package/knowledge/ui/presentation/gap.md +181 -0
- package/knowledge/ui/presentation/gap.vi.md +181 -0
- package/knowledge/ui/presentation/margin.md +168 -0
- package/knowledge/ui/presentation/margin.vi.md +171 -0
- package/knowledge/ui/presentation/measure.md +174 -0
- package/knowledge/ui/presentation/measure.vi.md +178 -0
- package/knowledge/ui/presentation/overflow.md +142 -0
- package/knowledge/ui/presentation/overflow.vi.md +142 -0
- package/knowledge/ui/presentation/padding.md +293 -0
- package/knowledge/ui/presentation/padding.vi.md +292 -0
- package/knowledge/ui/presentation/surface.md +166 -0
- package/knowledge/ui/presentation/surface.vi.md +166 -0
- package/knowledge/ui/presentation/text-flow.md +123 -0
- package/knowledge/ui/presentation/text-flow.vi.md +123 -0
- package/knowledge/ui/presentation/tone.md +114 -0
- package/knowledge/ui/presentation/tone.vi.md +113 -0
- package/knowledge/ui/proof/INDEX.md +55 -0
- package/knowledge/ui/proof/INDEX.vi.md +55 -0
- package/knowledge/ui/proof/accessibility.md +67 -0
- package/knowledge/ui/proof/accessibility.vi.md +68 -0
- package/knowledge/ui/proof/contrast.md +58 -0
- package/knowledge/ui/proof/contrast.vi.md +57 -0
- package/knowledge/ui/proof/focus.md +76 -0
- package/knowledge/ui/proof/focus.vi.md +76 -0
- package/knowledge/ui/proof/motion.md +63 -0
- package/knowledge/ui/proof/motion.vi.md +64 -0
- package/knowledge/ui/proof/render-truth.md +63 -0
- package/knowledge/ui/proof/render-truth.vi.md +63 -0
- package/operators/INDEX.md +199 -0
- package/operators/INDEX.vi.md +199 -0
- package/operators/architecture-decide/errors.json +115 -0
- package/operators/architecture-decide/operator.json +20 -0
- package/operators/architecture-decide/operator.md +133 -0
- package/operators/architecture-decide/operator.vi.md +130 -0
- package/operators/architecture-decide/self-test.mjs +221 -0
- package/operators/architecture-decide/validate.mjs +112 -0
- package/operators/backend-source-apply/errors.json +66 -0
- package/operators/backend-source-apply/operator.json +20 -0
- package/operators/backend-source-apply/operator.md +189 -0
- package/operators/backend-source-apply/operator.vi.md +185 -0
- package/operators/backend-source-apply/self-test.mjs +257 -0
- package/operators/backend-source-apply/validate.mjs +240 -0
- package/operators/business-decide/errors.json +78 -0
- package/operators/business-decide/operator.json +20 -0
- package/operators/business-decide/operator.md +176 -0
- package/operators/business-decide/operator.vi.md +174 -0
- package/operators/business-decide/self-test.mjs +225 -0
- package/operators/business-decide/validate.mjs +277 -0
- package/operators/content-generate/errors.json +106 -0
- package/operators/content-generate/operator.json +21 -0
- package/operators/content-generate/operator.md +155 -0
- package/operators/content-generate/operator.vi.md +155 -0
- package/operators/content-generate/self-test.mjs +288 -0
- package/operators/content-generate/validate.mjs +196 -0
- package/operators/errors.json +178 -0
- package/operators/frontend-direction-decide/errors.json +135 -0
- package/operators/frontend-direction-decide/operator.json +21 -0
- package/operators/frontend-direction-decide/operator.md +167 -0
- package/operators/frontend-direction-decide/operator.vi.md +167 -0
- package/operators/frontend-direction-decide/self-test.mjs +226 -0
- package/operators/frontend-direction-decide/validate.mjs +146 -0
- package/operators/frontend-presentation-resolve/errors.json +42 -0
- package/operators/frontend-presentation-resolve/operator.json +19 -0
- package/operators/frontend-presentation-resolve/operator.md +142 -0
- package/operators/frontend-presentation-resolve/operator.vi.md +140 -0
- package/operators/frontend-presentation-resolve/self-test.mjs +163 -0
- package/operators/frontend-presentation-resolve/validate.mjs +143 -0
- package/operators/frontend-source-apply/errors.json +30 -0
- package/operators/frontend-source-apply/operator.json +20 -0
- package/operators/frontend-source-apply/operator.md +127 -0
- package/operators/frontend-source-apply/operator.vi.md +126 -0
- package/operators/frontend-source-apply/self-test.mjs +214 -0
- package/operators/frontend-source-apply/validate.mjs +133 -0
- package/operators/frontend-surface-audit/errors.json +5 -0
- package/operators/frontend-surface-audit/operator.json +22 -0
- package/operators/frontend-surface-audit/operator.md +121 -0
- package/operators/frontend-surface-audit/operator.vi.md +121 -0
- package/operators/frontend-surface-audit/self-test.mjs +185 -0
- package/operators/frontend-surface-audit/validate.mjs +127 -0
- package/operators/git-publish/errors.json +54 -0
- package/operators/git-publish/operator.json +20 -0
- package/operators/git-publish/operator.md +180 -0
- package/operators/git-publish/operator.vi.md +178 -0
- package/operators/git-publish/self-test.mjs +177 -0
- package/operators/git-publish/validate.mjs +106 -0
- package/operators/platform-operate/errors.json +90 -0
- package/operators/platform-operate/operator.json +22 -0
- package/operators/platform-operate/operator.md +162 -0
- package/operators/platform-operate/operator.vi.md +160 -0
- package/operators/platform-operate/self-test.mjs +202 -0
- package/operators/platform-operate/validate.mjs +193 -0
- package/operators/quality-verify/errors.json +54 -0
- package/operators/quality-verify/operator.json +20 -0
- package/operators/quality-verify/operator.md +185 -0
- package/operators/quality-verify/operator.vi.md +177 -0
- package/operators/quality-verify/self-test.mjs +210 -0
- package/operators/quality-verify/validate.mjs +197 -0
- package/operators/release-deploy/errors.json +158 -0
- package/operators/release-deploy/operator.json +23 -0
- package/operators/release-deploy/operator.md +183 -0
- package/operators/release-deploy/operator.vi.md +181 -0
- package/operators/release-deploy/self-test.mjs +235 -0
- package/operators/release-deploy/validate.mjs +129 -0
- package/operators/uat-verify/errors.json +78 -0
- package/operators/uat-verify/operator.json +25 -0
- package/operators/uat-verify/operator.md +162 -0
- package/operators/uat-verify/operator.vi.md +161 -0
- package/operators/uat-verify/self-test.mjs +270 -0
- package/operators/uat-verify/validate.mjs +202 -0
- package/operators/workspace-bind/errors.json +90 -0
- package/operators/workspace-bind/operator.json +21 -0
- package/operators/workspace-bind/operator.md +148 -0
- package/operators/workspace-bind/operator.vi.md +148 -0
- package/operators/workspace-bind/self-test.mjs +205 -0
- package/operators/workspace-bind/validate.mjs +152 -0
- package/package.json +50 -0
- package/readiness/initialization/workspaces/commit-policy.json +91 -0
- package/readiness/initialization/workspaces/config.schema.json +35 -0
- package/readiness/initialization/workspaces/device-state.schema.json +54 -0
- package/readiness/initialization/workspaces/local-route.schema.json +206 -0
- package/readiness/initialization/workspaces/portable-route.schema.json +200 -0
- package/resources/INDEX.md +96 -0
- package/resources/INDEX.vi.md +99 -0
- package/resources/agents/profiles/claude.json +131 -0
- package/resources/agents/profiles/openai.json +131 -0
- package/resources/orchestrator.json +71 -0
- package/resources/tools.json +85 -0
- package/routing.json +272 -0
- package/scripts/alias-registry.mjs +31 -0
- package/scripts/device-state.mjs +497 -0
- package/scripts/device-state.spec.mjs +18 -0
- package/scripts/errors-registry.mjs +68 -0
- package/scripts/generate-alias-doc.mjs +66 -0
- package/scripts/generate-grammar-dna.mjs +387 -0
- package/scripts/generate-operators-index.mjs +95 -0
- package/scripts/generate-presentation-owned.mjs +681 -0
- package/scripts/install-cli.spec.mjs +74 -0
- package/scripts/json-schema.mjs +94 -0
- package/scripts/operator-md.mjs +96 -0
- package/scripts/run-operator-self-tests.mjs +36 -0
- package/scripts/validate-alias.mjs +165 -0
- package/scripts/validate-defaults.mjs +72 -0
- package/scripts/validate-knowledge-citations.mjs +90 -0
- package/scripts/validate-operator.mjs +125 -0
- package/scripts/validate-request.mjs +80 -0
- package/scripts/validate-resources.mjs +117 -0
- package/scripts/validate-response.mjs +141 -0
- package/scripts/validate-routing.mjs +91 -0
- package/scripts/validate-step.mjs +50 -0
- package/scripts/validate-templates.mjs +226 -0
- package/scripts/validate-templates.spec.mjs +144 -0
- package/scripts/validate-workflows.mjs +106 -0
- package/scripts/workspace-portable.mjs +389 -0
- package/scripts/workspace-portable.spec.mjs +246 -0
- package/templates/README.md +47 -0
- package/templates/README.vi.md +45 -0
- package/templates/changes.example.md +27 -0
- package/templates/grammars.template.md +34 -0
- package/templates/kinds/architecture-decision.contract.json +14 -0
- package/templates/kinds/architecture-decision.skeleton.md +52 -0
- package/templates/kinds/backend-source-application.contract.json +10 -0
- package/templates/kinds/backend-source-application.skeleton.md +34 -0
- package/templates/kinds/business-promise-authority.contract.json +13 -0
- package/templates/kinds/business-promise-authority.skeleton.md +67 -0
- package/templates/kinds/capture.schema.json +33 -0
- package/templates/kinds/changes.contract.json +10 -0
- package/templates/kinds/changes.skeleton.md +26 -0
- package/templates/kinds/checks.schema.json +44 -0
- package/templates/kinds/claims.schema.json +165 -0
- package/templates/kinds/conformance.schema.json +15 -0
- package/templates/kinds/content-brief.contract.json +12 -0
- package/templates/kinds/content-brief.skeleton.md +45 -0
- package/templates/kinds/content-generation-receipt.contract.json +11 -0
- package/templates/kinds/content-generation-receipt.skeleton.md +40 -0
- package/templates/kinds/content-review.contract.json +11 -0
- package/templates/kinds/content-review.skeleton.md +45 -0
- package/templates/kinds/contract.schema.json +28 -0
- package/templates/kinds/coverage-matrix.schema.json +86 -0
- package/templates/kinds/coverage.schema.json +62 -0
- package/templates/kinds/current-state.schema.json +40 -0
- package/templates/kinds/delta.schema.json +95 -0
- package/templates/kinds/e2e.schema.json +63 -0
- package/templates/kinds/frontend-direction-decision.contract.json +93 -0
- package/templates/kinds/frontend-direction-decision.skeleton.md +68 -0
- package/templates/kinds/frontend-presentation-resolution.contract.json +11 -0
- package/templates/kinds/frontend-presentation-resolution.skeleton.md +32 -0
- package/templates/kinds/frontend-source-application.contract.json +10 -0
- package/templates/kinds/frontend-source-application.skeleton.md +30 -0
- package/templates/kinds/frontend-surface-audit.contract.json +52 -0
- package/templates/kinds/frontend-surface-audit.skeleton.md +32 -0
- package/templates/kinds/gate-result.schema.json +64 -0
- package/templates/kinds/git-publication.contract.json +82 -0
- package/templates/kinds/git-publication.skeleton.md +61 -0
- package/templates/kinds/independent-critique.contract.json +9 -0
- package/templates/kinds/independent-critique.skeleton.md +28 -0
- package/templates/kinds/inventory.schema.json +35 -0
- package/templates/kinds/model.schema.json +99 -0
- package/templates/kinds/mutations.schema.json +65 -0
- package/templates/kinds/platform-operation-receipt.contract.json +76 -0
- package/templates/kinds/platform-operation-receipt.skeleton.md +54 -0
- package/templates/kinds/probes.schema.json +130 -0
- package/templates/kinds/proof.schema.json +17 -0
- package/templates/kinds/quality-verification.contract.json +87 -0
- package/templates/kinds/quality-verification.skeleton.md +58 -0
- package/templates/kinds/release-deployment.contract.json +85 -0
- package/templates/kinds/release-deployment.skeleton.md +67 -0
- package/templates/kinds/route.schema.json +293 -0
- package/templates/kinds/stack-model.schema.json +90 -0
- package/templates/kinds/uat-capture.schema.json +35 -0
- package/templates/kinds/uat-flow-verification.contract.json +11 -0
- package/templates/kinds/uat-flow-verification.skeleton.md +47 -0
- package/templates/kinds/uat-snapshot.schema.json +96 -0
- package/templates/kinds/uat-verdicts.schema.json +41 -0
- package/templates/kinds/ui-coverage.schema.json +77 -0
- package/templates/kinds/verdicts.schema.json +39 -0
- package/templates/kinds/workspace-route-binding.contract.json +12 -0
- package/templates/kinds/workspace-route-binding.skeleton.md +60 -0
- package/templates/kinds/writes.schema.json +37 -0
- package/templates/operator.template.md +73 -0
- package/templates/patterns.template.md +31 -0
- package/templates/step/request.schema.json +42 -0
- package/templates/step/response.schema.json +144 -0
- package/templates/ui-composition.template.md +36 -0
- package/templates/ui-presentation.template.md +57 -0
- package/templates/ui-proof.template.md +34 -0
- package/workflows/README.md +37 -0
- package/workflows/README.vi.md +37 -0
- package/workflows/backend-feature.json +59 -0
- package/workflows/content-unit.json +19 -0
- package/workflows/frontend-new-surface.json +81 -0
- package/workflows/frontend-reconstruct.json +67 -0
- package/workflows/frontend-refine.json +67 -0
- package/workflows/frontend-with-uat.json +78 -0
- package/workflows/full-feature.json +104 -0
- package/workflows/release.json +29 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// An operator.md package is closed when its own tables agree with each other, with the stop-code
|
|
2
|
+
// registry, with the kind contracts, and with its Vietnamese mirror: Params ↔ Requirements, Steps ↔
|
|
3
|
+
// Stops ↔ errors registry, Writes ↔ Outputs ↔ templates/kinds, Inputs ↔ templates/kinds, nested
|
|
4
|
+
// exchanges ↔ a waiting step, en ↔ vi.
|
|
5
|
+
import { existsSync } from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import process from 'node:process';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
import { loadOperatorPackages, cellCodes, cellParams, cellFiles, kindOf, isYes, exchangeOf } from './operator-md.mjs';
|
|
10
|
+
import { loadErrorsRegistry } from './errors-registry.mjs';
|
|
11
|
+
import { loadKindTemplates } from './validate-templates.mjs';
|
|
12
|
+
|
|
13
|
+
const unquote = (s) => String(s ?? '').trim().replace(/^`|`$/g, '');
|
|
14
|
+
const TABLES = ['context', 'inputs', 'requirements', 'steps', 'outputs', 'stops', 'next'];
|
|
15
|
+
|
|
16
|
+
export async function validateOperators(root) {
|
|
17
|
+
const errors = [];
|
|
18
|
+
const packages = await loadOperatorPackages(root);
|
|
19
|
+
const registry = await loadErrorsRegistry(root);
|
|
20
|
+
errors.push(...registry.errors);
|
|
21
|
+
const kinds = await loadKindTemplates(root);
|
|
22
|
+
const ids = new Set(packages.map((p) => p.manifest.id));
|
|
23
|
+
let checked = 0;
|
|
24
|
+
for (const pkg of packages.filter((p) => p.shape === 'v9')) {
|
|
25
|
+
checked += 1;
|
|
26
|
+
const id = pkg.manifest.id;
|
|
27
|
+
const at = `operators/${pkg.name}/operator.md`;
|
|
28
|
+
const op = pkg.en;
|
|
29
|
+
if (op.id !== id) errors.push(`${at}: title ${op.id} must equal operator.json id ${id}`);
|
|
30
|
+
for (const key of TABLES) if (!op.tables[key]) errors.push(`${at}: no ${key} table`);
|
|
31
|
+
if (TABLES.some((k) => !op.tables[k])) continue;
|
|
32
|
+
for (const legacy of ['context.md', 'input.md', 'execute.md', 'output.md', 'input.schema.json', 'output.schema.json', 'validate-input.mjs', 'validate-output.mjs', 'validation.mjs']) {
|
|
33
|
+
if (existsSync(path.join(pkg.dir, legacy))) errors.push(`operators/${pkg.name}/${legacy}: an operator.md package carries no ${legacy}`);
|
|
34
|
+
}
|
|
35
|
+
for (const needed of ['validate.mjs', 'self-test.mjs']) if (!existsSync(path.join(pkg.dir, needed))) errors.push(`operators/${pkg.name}/${needed}: missing`);
|
|
36
|
+
|
|
37
|
+
// Params ↔ Requirements.
|
|
38
|
+
const fields = new Map(op.tables.requirements.rows.map((r) => [unquote(r.field), r]));
|
|
39
|
+
const usedFields = new Set();
|
|
40
|
+
for (const s of op.tables.steps.rows) for (const p of cellParams(s.params)) { usedFields.add(p); if (!fields.has(p)) errors.push(`${at}:${s._line}: step ${s.n} reads param ${p}, which Requirements does not declare`); }
|
|
41
|
+
for (const [f, r] of fields) if (!usedFields.has(f)) errors.push(`${at}:${r._line}: requirement ${f} is read by no step`);
|
|
42
|
+
|
|
43
|
+
// Steps ↔ Stops ↔ registry.
|
|
44
|
+
const stops = new Map(op.tables.stops.rows.map((r) => [unquote(r.code), r]));
|
|
45
|
+
const usedCodes = new Set();
|
|
46
|
+
for (const s of op.tables.steps.rows) for (const c of cellCodes(s.stops)) { usedCodes.add(c); if (!stops.has(c)) errors.push(`${at}:${s._line}: step ${s.n} stops with ${c}, which the Stops table omits`); }
|
|
47
|
+
for (const [c, r] of stops) {
|
|
48
|
+
if (!usedCodes.has(c)) errors.push(`${at}:${r._line}: stop ${c} is emitted by no step`);
|
|
49
|
+
const entry = registry.codes[c];
|
|
50
|
+
if (!entry) { errors.push(`${at}:${r._line}: stop ${c} is in no errors registry`); continue; }
|
|
51
|
+
if (!registry.allowed(c, id)) errors.push(`${at}:${r._line}: stop ${c} is scoped to ${entry.scope.join(', ')}, not ${id}`);
|
|
52
|
+
if (r.disposition.trim() !== entry.disposition) errors.push(`${at}:${r._line}: stop ${c} says ${r.disposition.trim()} but ${entry.home} says ${entry.disposition}`);
|
|
53
|
+
if (entry.unless && !fields.has(entry.unless.param)) errors.push(`${entry.home}: ${c} unless names param ${entry.unless.param}, which ${id} does not declare`);
|
|
54
|
+
}
|
|
55
|
+
for (const [c, e] of Object.entries(registry.codes)) if (e.scope.length === 1 && e.scope[0] === id && !stops.has(c)) errors.push(`${e.home}: ${c} is defined for ${id} but its Stops table omits it`);
|
|
56
|
+
if (!cellCodes(op.tables.steps.rows[0]?.stops ?? '').includes('INVALID_INPUT')) errors.push(`${at}: step 1 must be the gate and stop with INVALID_INPUT`);
|
|
57
|
+
if (!cellFiles(op.tables.steps.rows[0]?.reads ?? '').length && !/request\/request\.json/.test(op.tables.steps.rows[0]?.reads ?? '')) errors.push(`${at}: step 1 must read request/request.json`);
|
|
58
|
+
|
|
59
|
+
// Writes ↔ Outputs ↔ templates/kinds.
|
|
60
|
+
const outputs = new Map();
|
|
61
|
+
for (const r of op.tables.outputs.rows) {
|
|
62
|
+
const kind = kindOf(r.kind); const file = unquote(r.file); const type = r.type.trim();
|
|
63
|
+
if (outputs.has(kind)) errors.push(`${at}:${r._line}: output ${kind} is declared twice`);
|
|
64
|
+
outputs.set(kind, { ...r, file, type });
|
|
65
|
+
const inResponse = /^(?:[a-z][a-z-]*\/)?response\//.test(file);
|
|
66
|
+
if (!inResponse) errors.push(`${at}:${r._line}: output ${kind} must live under response/ or <exchange>/response/`);
|
|
67
|
+
if (type === 'md') { if (!kinds.has(kind)) errors.push(`${at}:${r._line}: output ${kind} is md but templates/kinds/${kind}.contract.json is missing`); if (!file.endsWith('.md')) errors.push(`${at}:${r._line}: md output ${kind} must be a .md file`); }
|
|
68
|
+
if (type === 'data') { if (!existsSync(path.join(root, 'templates', 'kinds', `${kind}.schema.json`))) errors.push(`${at}:${r._line}: output ${kind} is data but templates/kinds/${kind}.schema.json is missing`); if (!/\/data\/.+\.json$/.test(file)) errors.push(`${at}:${r._line}: data output ${kind} must live under response/data/ as .json`); }
|
|
69
|
+
if (type === 'artifact' && !/\/artifacts\//.test(file)) errors.push(`${at}:${r._line}: artifact output ${kind} must live under response/artifacts/`);
|
|
70
|
+
}
|
|
71
|
+
const written = new Set();
|
|
72
|
+
for (const s of op.tables.steps.rows) for (const f of cellFiles(s.writes)) written.add(f);
|
|
73
|
+
const outputFiles = new Set([...outputs.values()].map((o) => o.file));
|
|
74
|
+
for (const f of written) if (f !== 'response/response.json' && !outputFiles.has(f) && !exchangeOf(f)) errors.push(`${at}: a step writes ${f}, which Outputs does not declare`);
|
|
75
|
+
for (const f of outputFiles) if (!written.has(f) && !exchangeOf(f)) errors.push(`${at}: output ${f} is written by no step`);
|
|
76
|
+
if (!written.has('response/response.json')) errors.push(`${at}: no step writes response/response.json`);
|
|
77
|
+
|
|
78
|
+
// Nested exchanges: an Output under <exchange>/response/ needs one step that waits for it and reads it.
|
|
79
|
+
for (const ex of new Set([...outputFiles].map(exchangeOf).filter(Boolean))) {
|
|
80
|
+
const waiting = op.tables.steps.rows.filter((s) => /response\/response\.json/.test(s.writes) && /waiting/.test(s.writes) && s.reads.includes(`${ex}/response/`));
|
|
81
|
+
if (waiting.length !== 1) errors.push(`${at}: exchange ${ex} needs exactly one step that writes response/response.json (waiting) and reads ${ex}/response/…, found ${waiting.length}`);
|
|
82
|
+
for (const s of op.tables.steps.rows) for (const f of cellFiles(s.writes)) if (exchangeOf(f) === ex) errors.push(`${at}:${s._line}: step ${s.n} writes ${f}; only the exchange's own agent writes under ${ex}/response/`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Inputs ↔ templates/kinds; Next ↔ operators.
|
|
86
|
+
for (const r of op.tables.inputs.rows) {
|
|
87
|
+
const kind = kindOf(r.kind);
|
|
88
|
+
if (kind === '—' || kind === '') continue;
|
|
89
|
+
if (!kinds.has(kind) && !existsSync(path.join(root, 'templates', 'kinds', `${kind}.schema.json`))) errors.push(`${at}:${r._line}: input ${kind} has no contract or schema under templates/kinds`);
|
|
90
|
+
}
|
|
91
|
+
// Next may hand to another operator, or end automation the way routing.json does: `user` (a person
|
|
92
|
+
// decides or publishes) or `external` (the blocker is outside the runtime).
|
|
93
|
+
for (const r of op.tables.next.rows) { const target = unquote(r.operator); if (!ids.has(target) && target !== 'user' && target !== 'external') errors.push(`${at}:${r._line}: next names unknown operator ${target}`); }
|
|
94
|
+
|
|
95
|
+
// en ↔ vi.
|
|
96
|
+
if (!pkg.vi) { errors.push(`operators/${pkg.name}/operator.vi.md: missing`); continue; }
|
|
97
|
+
const vi = pkg.vi;
|
|
98
|
+
if (vi.id !== id) errors.push(`operators/${pkg.name}/operator.vi.md: title must be ${id}`);
|
|
99
|
+
for (const key of TABLES) {
|
|
100
|
+
const a = op.tables[key]; const b = vi.tables[key];
|
|
101
|
+
if (!b) { errors.push(`operators/${pkg.name}/operator.vi.md: no ${key} table`); continue; }
|
|
102
|
+
if (a.rows.length !== b.rows.length) { errors.push(`operators/${pkg.name}/operator.vi.md: ${key} has ${b.rows.length} rows, English has ${a.rows.length}`); continue; }
|
|
103
|
+
const firstKey = { context: 'alias', inputs: 'kind', requirements: 'field', steps: 'n', outputs: 'kind', stops: 'code', next: 'operator' }[key];
|
|
104
|
+
a.rows.forEach((ra, i) => {
|
|
105
|
+
const rb = b.rows[i];
|
|
106
|
+
if (unquote(ra[firstKey]) !== unquote(rb[firstKey])) errors.push(`operators/${pkg.name}/operator.vi.md:${rb._line}: ${key} row ${i + 1} is ${unquote(rb[firstKey])}, English has ${unquote(ra[firstKey])}`);
|
|
107
|
+
if (key === 'steps') {
|
|
108
|
+
if (cellCodes(ra.stops).join() !== cellCodes(rb.stops).join()) errors.push(`operators/${pkg.name}/operator.vi.md:${rb._line}: step ${ra.n} stop codes differ from English`);
|
|
109
|
+
if (cellParams(ra.params).join() !== cellParams(rb.params).join()) errors.push(`operators/${pkg.name}/operator.vi.md:${rb._line}: step ${ra.n} params differ from English`);
|
|
110
|
+
if (cellFiles(ra.writes).join() !== cellFiles(rb.writes).join()) errors.push(`operators/${pkg.name}/operator.vi.md:${rb._line}: step ${ra.n} writes differ from English`);
|
|
111
|
+
}
|
|
112
|
+
if (key === 'stops' && ra.disposition.trim() !== rb.disposition.trim()) errors.push(`operators/${pkg.name}/operator.vi.md:${rb._line}: ${unquote(ra.code)} disposition differs from English`);
|
|
113
|
+
if (key === 'outputs' && (ra.type.trim() !== rb.type.trim() || unquote(ra.file) !== unquote(rb.file))) errors.push(`operators/${pkg.name}/operator.vi.md:${rb._line}: output ${unquote(ra.kind)} file or type differs from English`);
|
|
114
|
+
if (key === 'requirements' && unquote(ra.default) !== unquote(rb.default)) errors.push(`operators/${pkg.name}/operator.vi.md:${rb._line}: requirement ${unquote(ra.field)} default differs from English`);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return { errors, checked, total: packages.length };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
122
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
123
|
+
const { errors, checked, total } = await validateOperators(root);
|
|
124
|
+
if (errors.length) { process.stderr.write(`${errors.join('\n')}\n`); process.exitCode = 1; } else process.stdout.write(`operators closed: ${checked} operator.md packages of ${total}\n`);
|
|
125
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// The request half of one branch (step-N/parallel-M/request/request.json), checked before any agent
|
|
2
|
+
// runs: the gate schema; the operator exists and is an operator.md package; every requirement key is
|
|
3
|
+
// one the operator declares and every required one (Default —) has a value; every declared input is
|
|
4
|
+
// present when required, points inside the session, and the file exists; a nested exchange's request
|
|
5
|
+
// names an exchange the operator's Outputs declare. A request that fails here is the orchestrator's
|
|
6
|
+
// or the person's mistake, never the agent's.
|
|
7
|
+
import { existsSync } from 'node:fs';
|
|
8
|
+
import { readFile } from 'node:fs/promises';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import process from 'node:process';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
12
|
+
import { validateAgainst } from './json-schema.mjs';
|
|
13
|
+
import { loadOperatorPackages, kindOf, isYes, exchangeOf } from './operator-md.mjs';
|
|
14
|
+
|
|
15
|
+
const unquote = (s) => String(s ?? '').trim().replace(/^`|`$/g, '');
|
|
16
|
+
export const isRequiredField = (row) => row.default.trim().startsWith('—');
|
|
17
|
+
export const isEmpty = (v) => v === undefined || v === null || v === '' || v === '—';
|
|
18
|
+
|
|
19
|
+
// A branch dir is <session>/step-N/parallel-M; an exchange dir is <branch>/<exchange>.
|
|
20
|
+
export function sessionRootOf(dir) {
|
|
21
|
+
let cur = path.resolve(dir);
|
|
22
|
+
for (let i = 0; i < 4; i += 1) { if (/^step-\d+$/.test(path.basename(cur))) return path.dirname(cur); cur = path.dirname(cur); }
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function validateRequest(root, dir, packages) {
|
|
27
|
+
const errors = [];
|
|
28
|
+
const rel = (f) => path.relative(sessionRootOf(dir) ?? dir, path.join(dir, f)).split(path.sep).join('/');
|
|
29
|
+
const file = path.join(dir, 'request', 'request.json');
|
|
30
|
+
if (!existsSync(file)) return { errors: [`${rel('request/request.json')}: missing`], request: null };
|
|
31
|
+
let request; try { request = JSON.parse(await readFile(file, 'utf8')); } catch (e) { return { errors: [`${rel('request/request.json')}: ${e.message}`], request: null }; }
|
|
32
|
+
const schema = JSON.parse(await readFile(path.join(root, 'templates', 'step', 'request.schema.json'), 'utf8'));
|
|
33
|
+
errors.push(...validateAgainst(schema, request, rel('request/request.json')));
|
|
34
|
+
packages ??= await loadOperatorPackages(root);
|
|
35
|
+
const pkg = packages.find((p) => p.manifest.id === request.operatorId);
|
|
36
|
+
if (!pkg) { errors.push(`request.json: unknown operator ${request.operatorId}`); return { errors, request }; }
|
|
37
|
+
if (pkg.shape !== 'v9') { errors.push(`${request.operatorId} is not an operator.md package`); return { errors, request }; }
|
|
38
|
+
const op = pkg.en;
|
|
39
|
+
const sessionRoot = sessionRootOf(dir);
|
|
40
|
+
|
|
41
|
+
if (request.exchange) {
|
|
42
|
+
// A nested exchange: it must be one the operator's Outputs declare, and it carries no person-facing requirements.
|
|
43
|
+
const declared = (op.tables.outputs?.rows ?? []).map((r) => exchangeOf(unquote(r.file))).filter(Boolean);
|
|
44
|
+
if (!declared.includes(request.exchange)) errors.push(`request.json: exchange ${request.exchange} is not declared by an Output of ${op.id}`);
|
|
45
|
+
if (Object.keys(request.requirements ?? {}).length) errors.push('request.json: a nested exchange carries no requirements');
|
|
46
|
+
} else {
|
|
47
|
+
const declared = new Map((op.tables.requirements?.rows ?? []).map((r) => [unquote(r.field), r]));
|
|
48
|
+
for (const key of Object.keys(request.requirements ?? {})) if (!declared.has(key)) errors.push(`request.json: requirements.${key} is not a field ${op.id} declares`);
|
|
49
|
+
for (const [key, row] of declared) if (isRequiredField(row) && isEmpty(request.requirements?.[key])) errors.push(`request.json: required field ${key} has no value`);
|
|
50
|
+
const declaredInputs = new Map((op.tables.inputs?.rows ?? []).map((r) => [kindOf(r.kind), r]));
|
|
51
|
+
for (const kind of Object.keys(request.inputs ?? {})) if (!declaredInputs.has(kind)) errors.push(`request.json: inputs.${kind} is not an Input ${op.id} declares`);
|
|
52
|
+
for (const [kind, row] of declaredInputs) if (isYes(row.required) && isEmpty(request.inputs?.[kind])) errors.push(`request.json: required input ${kind} is absent`);
|
|
53
|
+
}
|
|
54
|
+
for (const [kind, p] of Object.entries(request.inputs ?? {})) {
|
|
55
|
+
if (!sessionRoot) { errors.push(`request.json: inputs.${kind} cannot be resolved; the branch is not under a session`); continue; }
|
|
56
|
+
if (!existsSync(path.join(sessionRoot, p))) errors.push(`request.json: inputs.${kind} = ${p} does not exist in the session`);
|
|
57
|
+
}
|
|
58
|
+
// The orchestrator hashes every request into state.json; a request that changed since is tampering.
|
|
59
|
+
if (sessionRoot && existsSync(path.join(sessionRoot, 'state.json'))) {
|
|
60
|
+
try {
|
|
61
|
+
const state = JSON.parse(await readFile(path.join(sessionRoot, 'state.json'), 'utf8'));
|
|
62
|
+
const key = `${request.step}/${request.parallel}${request.exchange ? `/${request.exchange}` : ''}`;
|
|
63
|
+
const expected = state.requestHashes?.[key];
|
|
64
|
+
if (expected) {
|
|
65
|
+
const { createHash } = await import('node:crypto');
|
|
66
|
+
const actual = `sha256:${createHash('sha256').update(await readFile(file)).digest('hex')}`;
|
|
67
|
+
if (actual !== expected) errors.push(`request.json: hash ${actual} differs from state.json requestHashes[${key}]`);
|
|
68
|
+
}
|
|
69
|
+
} catch (e) { errors.push(`state.json: ${e.message}`); }
|
|
70
|
+
}
|
|
71
|
+
return { errors, request, pkg };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
75
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
76
|
+
const target = process.argv[2];
|
|
77
|
+
if (!target) { process.stderr.write('usage: node scripts/validate-request.mjs <session>/step-N/parallel-M[/<exchange>]\n'); process.exit(2); }
|
|
78
|
+
const { errors } = await validateRequest(root, path.resolve(target));
|
|
79
|
+
if (errors.length) { process.stderr.write(`${errors.join('\n')}\n`); process.exitCode = 1; } else process.stdout.write('request valid\n');
|
|
80
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// Resources close when: every profile declares, for every tool in resources/tools.json, what the runtime can do
|
|
2
|
+
// (capabilities) and what the profile is allowed to do (permits), and never permits what it cannot; every
|
|
3
|
+
// operator binds one profile and declares its tools as @tools/<id> with one mode the registry defines, each
|
|
4
|
+
// permitted by its profile and supported by its runtime; resources/INDEX.md repeats the bindings exactly;
|
|
5
|
+
// orchestrator.json fixes the agent contract and a symmetric profile equivalent on the other runtime.
|
|
6
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import process from 'node:process';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
|
|
11
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
12
|
+
const errors = [];
|
|
13
|
+
|
|
14
|
+
const toolsDoc = JSON.parse(await readFile(path.join(root, 'resources', 'tools.json'), 'utf8'));
|
|
15
|
+
if (toolsDoc.schemaVersion !== 9) errors.push('resources/tools.json: schemaVersion must be 9');
|
|
16
|
+
const TOOLS = toolsDoc.tools ?? {};
|
|
17
|
+
const RUNTIMES = Object.keys(toolsDoc.runtimes ?? {});
|
|
18
|
+
for (const [id, tool] of Object.entries(TOOLS)) {
|
|
19
|
+
if (!/^[a-z][a-z0-9-]*$/.test(id)) errors.push(`resources/tools.json: tool id ${id} must be lowercase`);
|
|
20
|
+
if (!tool.purpose) errors.push(`resources/tools.json: ${id} needs a purpose`);
|
|
21
|
+
if (!tool.modes || !Object.keys(tool.modes).length) errors.push(`resources/tools.json: ${id} needs modes`);
|
|
22
|
+
for (const rt of RUNTIMES) if (typeof tool.support?.[rt]?.supported !== 'boolean' || !tool.support?.[rt]?.via) errors.push(`resources/tools.json: ${id} must say whether ${rt} supports it and via what`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// One file per runtime under resources/agents/profiles; the file name is the runtime id.
|
|
26
|
+
const profilesDir = path.join(root, 'resources', 'agents', 'profiles');
|
|
27
|
+
const profiles = {};
|
|
28
|
+
for (const file of (await readdir(profilesDir)).filter((f) => f.endsWith('.json')).sort()) {
|
|
29
|
+
const runtime = file.slice(0, -'.json'.length);
|
|
30
|
+
const group = JSON.parse(await readFile(path.join(profilesDir, file), 'utf8'));
|
|
31
|
+
if (group.runtime !== runtime) errors.push(`${file}: runtime must be "${runtime}", got "${group.runtime}"`);
|
|
32
|
+
if (!RUNTIMES.includes(runtime)) errors.push(`${file}: runtime ${runtime} is not declared in resources/tools.json`);
|
|
33
|
+
if (typeof group.provider !== 'string') errors.push(`runtime ${runtime}: provider must be a string`);
|
|
34
|
+
for (const [id, profile] of Object.entries(group.profiles ?? {})) {
|
|
35
|
+
if (profiles[id]) errors.push(`profile ${id}: declared in more than one runtime`);
|
|
36
|
+
profiles[id] = { ...profile, runtime, provider: group.provider };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
for (const [id, profile] of Object.entries(profiles)) {
|
|
40
|
+
for (const tool of Object.keys(TOOLS)) {
|
|
41
|
+
const supported = TOOLS[tool].support?.[profile.runtime]?.supported === true;
|
|
42
|
+
if (typeof profile.capabilities?.[tool] !== 'boolean') errors.push(`profile ${id}: capabilities.${tool} must be true or false`);
|
|
43
|
+
if (typeof profile.permits?.[tool] !== 'boolean') errors.push(`profile ${id}: permits.${tool} must be true or false`);
|
|
44
|
+
if (profile.capabilities?.[tool] === true && !supported) errors.push(`profile ${id}: capabilities.${tool} is true but resources/tools.json says ${profile.runtime} does not support it`);
|
|
45
|
+
if (profile.permits?.[tool] === true && profile.capabilities?.[tool] !== true) errors.push(`profile ${id}: permits.${tool} but capabilities.${tool} is false`);
|
|
46
|
+
}
|
|
47
|
+
for (const key of Object.keys(profile.permits ?? {})) if (!TOOLS[key]) errors.push(`profile ${id}: permits unknown tool ${key}`);
|
|
48
|
+
if (profile.isolation !== 'fresh' || profile.forkTurns !== 'none') errors.push(`profile ${id}: material executions are fresh with no inherited turns`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Each operator binds one profile and declares its tools with modes.
|
|
52
|
+
const operatorsDir = path.join(root, 'operators');
|
|
53
|
+
const operators = new Map();
|
|
54
|
+
for (const entry of await readdir(operatorsDir, { withFileTypes: true })) {
|
|
55
|
+
if (!entry.isDirectory()) continue;
|
|
56
|
+
const manifest = JSON.parse(await readFile(path.join(operatorsDir, entry.name, 'operator.json'), 'utf8'));
|
|
57
|
+
operators.set(manifest.id, manifest.resources);
|
|
58
|
+
}
|
|
59
|
+
const used = new Set();
|
|
60
|
+
for (const [id, resources] of operators) {
|
|
61
|
+
if (!resources || typeof resources !== 'object') { errors.push(`${id}: operator.json must declare resources { profile, grammarBound, tools }`); continue; }
|
|
62
|
+
const { profile: profileId, grammarBound, tools = {} } = resources;
|
|
63
|
+
const profile = profiles[profileId];
|
|
64
|
+
if (typeof profileId !== 'string' || !profile) { errors.push(`${id}: resources.profile ${profileId} is not a declared profile`); continue; }
|
|
65
|
+
used.add(profileId);
|
|
66
|
+
if (typeof grammarBound !== 'boolean') errors.push(`${id}: resources.grammarBound must be true or false`);
|
|
67
|
+
if (resources.requires || resources.policy) errors.push(`${id}: resources.requires and resources.policy are retired; declare resources.tools`);
|
|
68
|
+
for (const [ref, mode] of Object.entries(tools)) {
|
|
69
|
+
const m = /^@tools\/([a-z][a-z0-9-]*)$/.exec(ref);
|
|
70
|
+
if (!m) { errors.push(`${id}: tool ${ref} must be written @tools/<id>`); continue; }
|
|
71
|
+
const tool = TOOLS[m[1]];
|
|
72
|
+
if (!tool) { errors.push(`${id}: ${ref} is not in resources/tools.json`); continue; }
|
|
73
|
+
if (!tool.modes[mode]) errors.push(`${id}: ${ref} mode ${mode} is not one of ${Object.keys(tool.modes).join(', ')}`);
|
|
74
|
+
if (mode === 'never') errors.push(`${id}: ${ref} declared as never; leave it out instead`);
|
|
75
|
+
if (!profile.permits?.[m[1]]) errors.push(`${id}: declares ${ref} but profile ${profileId} does not permit it`);
|
|
76
|
+
}
|
|
77
|
+
if (!tools['@tools/fileread']) errors.push(`${id}: every operator reads its Context aliases; declare @tools/fileread`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// resources/INDEX.md repeats the bindings: | `operator` | profile | grammar | tools | why |
|
|
81
|
+
const index = await readFile(path.join(root, 'resources', 'INDEX.md'), 'utf8');
|
|
82
|
+
const GRAMMAR_WORD = { true: 'yes', false: 'no' };
|
|
83
|
+
const rows = new Map();
|
|
84
|
+
for (const line of index.split(/\r?\n/)) {
|
|
85
|
+
const m = /^\| `([a-z.]+)` \| ([a-z-]+) \| (yes|no) \| ([^|]*) \|/.exec(line);
|
|
86
|
+
if (m) rows.set(m[1], { profile: m[2], grammar: m[3], tools: m[4].trim() });
|
|
87
|
+
}
|
|
88
|
+
for (const [id, resources] of operators) {
|
|
89
|
+
const row = rows.get(id);
|
|
90
|
+
if (!row) { errors.push(`resources/INDEX.md: no process-matrix row for ${id}`); continue; }
|
|
91
|
+
if (!resources) continue;
|
|
92
|
+
const expectTools = Object.entries(resources.tools ?? {}).map(([k, v]) => `\`${k.split('/')[1]}:${v}\``).join(', ');
|
|
93
|
+
const expect = { profile: resources.profile, grammar: GRAMMAR_WORD[resources.grammarBound], tools: expectTools };
|
|
94
|
+
for (const key of Object.keys(expect)) if (row[key] !== expect[key]) errors.push(`resources/INDEX.md: ${id} row says ${key} "${row[key]}", operator.json says "${expect[key]}"`);
|
|
95
|
+
}
|
|
96
|
+
for (const id of rows.keys()) if (!operators.has(id)) errors.push(`resources/INDEX.md: row for unknown operator ${id}`);
|
|
97
|
+
|
|
98
|
+
// The orchestrator record turns operators into agents.
|
|
99
|
+
const orchestrator = JSON.parse(await readFile(path.join(root, 'resources', 'orchestrator.json'), 'utf8'));
|
|
100
|
+
if (orchestrator.agentPerOperator !== true) errors.push('orchestrator.json: agentPerOperator must be true');
|
|
101
|
+
if (!Number.isInteger(orchestrator.maxConcurrentAgents) || orchestrator.maxConcurrentAgents < 1 || orchestrator.maxConcurrentAgents > 3) errors.push('orchestrator.json: maxConcurrentAgents must be an integer from 1 to 3');
|
|
102
|
+
if (orchestrator.dispatch !== 'routing.json') errors.push('orchestrator.json: dispatch must be routing.json');
|
|
103
|
+
const pairs = orchestrator.profileEquivalents?.pairs ?? {};
|
|
104
|
+
for (const [id, profile] of Object.entries(profiles)) {
|
|
105
|
+
const eq = pairs[id];
|
|
106
|
+
if (!eq) { errors.push(`orchestrator.json: profileEquivalents lacks ${id}`); continue; }
|
|
107
|
+
if (!profiles[eq]) errors.push(`orchestrator.json: profileEquivalents maps ${id} to unknown profile ${eq}`);
|
|
108
|
+
else if (profiles[eq].runtime === profile.runtime) errors.push(`orchestrator.json: profileEquivalents maps ${id} to ${eq} on the same runtime`);
|
|
109
|
+
else if (pairs[eq] !== id) errors.push(`orchestrator.json: profileEquivalents is not symmetric for ${id} and ${eq}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (errors.length > 0) {
|
|
113
|
+
process.stderr.write(`${errors.join('\n')}\n`);
|
|
114
|
+
process.exitCode = 1;
|
|
115
|
+
} else {
|
|
116
|
+
process.stdout.write(`resources bound: ${RUNTIMES.length} runtimes, ${Object.keys(TOOLS).length} tools, ${Object.keys(profiles).length} profiles, ${operators.size} operators on ${used.size} profiles\n`);
|
|
117
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// The response half of one branch (step-N/parallel-M/response/), checked after the agent stops: the
|
|
2
|
+
// gate schema; every Output the operator's operator.md declares is present when required and valid
|
|
3
|
+
// when present (markdown kinds through templates/kinds/<kind>.contract.json, data kinds through
|
|
4
|
+
// <kind>.schema.json, artifacts by existence); a blocked stop is a code the operator may emit whose
|
|
5
|
+
// effective disposition is terminate; a taken fallback is one whose effective disposition is fallback
|
|
6
|
+
// and is recorded under ## Fallbacks taken; a waiting status names a declared exchange; a nested
|
|
7
|
+
// exchange's own response is checked the same way. Effective means after `unless` is evaluated
|
|
8
|
+
// against request.json requirements. Operator-specific law lives in operators/<id>/validate.mjs.
|
|
9
|
+
import { existsSync } from 'node:fs';
|
|
10
|
+
import { readFile } from 'node:fs/promises';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import process from 'node:process';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
import { validateAgainst } from './json-schema.mjs';
|
|
15
|
+
import { checkDocument, loadKindTemplates } from './validate-templates.mjs';
|
|
16
|
+
import { loadOperatorPackages, kindOf, isYes, exchangeOf } from './operator-md.mjs';
|
|
17
|
+
import { loadErrorsRegistry } from './errors-registry.mjs';
|
|
18
|
+
import { sessionRootOf } from './validate-request.mjs';
|
|
19
|
+
|
|
20
|
+
const unquote = (s) => String(s ?? '').trim().replace(/^`|`$/g, '');
|
|
21
|
+
|
|
22
|
+
// Rows of the first table under `## <heading>`, cells unquoted.
|
|
23
|
+
export function tableUnder(text, heading) {
|
|
24
|
+
const lines = text.split(/\r?\n/);
|
|
25
|
+
const start = lines.findIndex((l) => l.trim() === heading);
|
|
26
|
+
if (start === -1) return null;
|
|
27
|
+
const rows = [];
|
|
28
|
+
let inTable = false;
|
|
29
|
+
for (let i = start + 1; i < lines.length && !lines[i].startsWith('## '); i += 1) {
|
|
30
|
+
if (lines[i].startsWith('|') && /^\|\s*-{3,}/.test(lines[i + 1] ?? '')) { inTable = true; i += 1; continue; }
|
|
31
|
+
if (inTable) { if (!lines[i].startsWith('|')) break; rows.push(lines[i].split('|').slice(1, -1).map((c) => unquote(c))); }
|
|
32
|
+
}
|
|
33
|
+
return rows;
|
|
34
|
+
}
|
|
35
|
+
function patternOf(fileCell) {
|
|
36
|
+
const esc = unquote(fileCell).replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/<[^>]+>/g, '[A-Za-z0-9_.-]+');
|
|
37
|
+
return new RegExp(`^${esc}$`);
|
|
38
|
+
}
|
|
39
|
+
export function effectiveDisposition(entry, requirements) {
|
|
40
|
+
if (entry.unless && String(requirements?.[entry.unless.param] ?? '') === String(entry.unless.equals)) return entry.unless.then;
|
|
41
|
+
return entry.disposition;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// `dir` is the branch (or exchange) folder; `requirements` come from the branch's request.json.
|
|
45
|
+
export async function validateResponse(root, dir, { requirements = {}, exchange = null, packages, kinds, registry } = {}) {
|
|
46
|
+
const errors = [];
|
|
47
|
+
const rel = (f) => path.relative(sessionRootOf(dir) ?? dir, path.join(dir, f)).split(path.sep).join('/');
|
|
48
|
+
const file = path.join(dir, 'response', 'response.json');
|
|
49
|
+
if (!existsSync(file)) return { errors: [`${rel('response/response.json')}: missing`], response: null, present: new Set() };
|
|
50
|
+
let response; try { response = JSON.parse(await readFile(file, 'utf8')); } catch (e) { return { errors: [`${rel('response/response.json')}: ${e.message}`], response: null, present: new Set() }; }
|
|
51
|
+
const schema = JSON.parse(await readFile(path.join(root, 'templates', 'step', 'response.schema.json'), 'utf8'));
|
|
52
|
+
errors.push(...validateAgainst(schema, response, rel('response/response.json')));
|
|
53
|
+
if (response.status !== 'blocked' && response.stop !== undefined) errors.push(`${rel('response/response.json')}: only a blocked response carries a stop`);
|
|
54
|
+
if (response.status !== 'waiting' && response.awaiting !== undefined) errors.push(`${rel('response/response.json')}: only a waiting response carries awaiting`);
|
|
55
|
+
if ((response.exchange ?? null) !== exchange) errors.push(`${rel('response/response.json')}: exchange ${response.exchange ?? 'none'} does not match the folder ${exchange ?? 'none'}`);
|
|
56
|
+
packages ??= await loadOperatorPackages(root);
|
|
57
|
+
const pkg = packages.find((p) => p.manifest.id === response.operatorId);
|
|
58
|
+
if (!pkg) { errors.push(`response.json: unknown operator ${response.operatorId}`); return { errors, response, present: new Set() }; }
|
|
59
|
+
if (pkg.shape !== 'v9') { errors.push(`${response.operatorId} is not an operator.md package`); return { errors, response, present: new Set() }; }
|
|
60
|
+
kinds ??= await loadKindTemplates(root);
|
|
61
|
+
registry ??= await loadErrorsRegistry(root);
|
|
62
|
+
errors.push(...registry.errors);
|
|
63
|
+
const op = pkg.en;
|
|
64
|
+
|
|
65
|
+
// Which Outputs belong to this folder: the branch owns files without an exchange prefix, an exchange owns its own.
|
|
66
|
+
const outputs = (op.tables.outputs?.rows ?? []).filter((r) => exchangeOf(unquote(r.file)) === exchange);
|
|
67
|
+
const present = new Set();
|
|
68
|
+
for (const row of outputs) {
|
|
69
|
+
const kind = kindOf(row.kind);
|
|
70
|
+
const type = row.type.trim();
|
|
71
|
+
// Inside an exchange folder the declared file is <exchange>/response/x; the response.json there lists response/x.
|
|
72
|
+
const declaredFile = exchange ? unquote(row.file).replace(`${exchange}/`, '') : unquote(row.file);
|
|
73
|
+
const value = response.fields?.[kind];
|
|
74
|
+
const files = value === undefined ? [] : Array.isArray(value) ? value : [value];
|
|
75
|
+
if (files.length === 0) { if (isYes(row.required) && response.status === 'done') errors.push(`${rel('response/response.json')}: required output ${kind} is not in fields`); continue; }
|
|
76
|
+
present.add(kind);
|
|
77
|
+
const re = patternOf(declaredFile);
|
|
78
|
+
for (const f of files) {
|
|
79
|
+
if (!re.test(f)) errors.push(`${rel('response/response.json')}: fields.${kind} = ${f} does not match the declared file ${declaredFile}`);
|
|
80
|
+
const full = path.join(dir, f);
|
|
81
|
+
if (!existsSync(full)) { errors.push(`${rel(f)}: listed in response.json but missing`); continue; }
|
|
82
|
+
if (type === 'md') {
|
|
83
|
+
const contract = kinds.get(kind);
|
|
84
|
+
if (!contract) { errors.push(`templates/kinds/${kind}.contract.json: missing`); continue; }
|
|
85
|
+
errors.push(...checkDocument(rel(f), await readFile(full, 'utf8'), contract, 'en'));
|
|
86
|
+
} else if (type === 'data') {
|
|
87
|
+
const schemaPath = path.join(root, 'templates', 'kinds', `${kind}.schema.json`);
|
|
88
|
+
if (!existsSync(schemaPath)) { errors.push(`templates/kinds/${kind}.schema.json: missing`); continue; }
|
|
89
|
+
let value2; try { value2 = JSON.parse(await readFile(full, 'utf8')); } catch (e) { errors.push(`${rel(f)}: ${e.message}`); continue; }
|
|
90
|
+
errors.push(...validateAgainst(JSON.parse(await readFile(schemaPath, 'utf8')), value2, rel(f)));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
for (const kind of Object.keys(response.fields ?? {})) if (!outputs.some((r) => kindOf(r.kind) === kind)) errors.push(`${rel('response/response.json')}: fields.${kind} is not an Output of ${op.id}${exchange ? ` in exchange ${exchange}` : ''}`);
|
|
95
|
+
|
|
96
|
+
// Stops, fallbacks, waiting.
|
|
97
|
+
const stopsTable = new Set((op.tables.stops?.rows ?? []).map((r) => unquote(r.code)));
|
|
98
|
+
const dispositionOf = (code) => { const e = registry.codes[code]; return e && registry.allowed(code, op.id) ? effectiveDisposition(e, requirements) : null; };
|
|
99
|
+
if (response.status === 'blocked') {
|
|
100
|
+
const d = dispositionOf(response.stop);
|
|
101
|
+
if (!stopsTable.has(response.stop)) errors.push(`${rel('response/response.json')}: stop ${response.stop} is not in the Stops table of ${op.id}`);
|
|
102
|
+
if (d === null) errors.push(`${rel('response/response.json')}: stop ${response.stop} is not a registered code ${op.id} may emit`);
|
|
103
|
+
else if (d !== 'terminate') errors.push(`${rel('response/response.json')}: ${response.stop} has disposition fallback under these requirements; the step should have continued`);
|
|
104
|
+
}
|
|
105
|
+
for (const code of response.fallbacks ?? []) {
|
|
106
|
+
const d = dispositionOf(code);
|
|
107
|
+
if (!stopsTable.has(code)) errors.push(`${rel('response/response.json')}: fallback ${code} is not in the Stops table of ${op.id}`);
|
|
108
|
+
if (d === null) errors.push(`${rel('response/response.json')}: fallback ${code} is not a registered code ${op.id} may emit`);
|
|
109
|
+
else if (d !== 'fallback') errors.push(`${rel('response/response.json')}: ${code} has disposition terminate under these requirements; it cannot be taken as a fallback`);
|
|
110
|
+
}
|
|
111
|
+
if (response.status === 'waiting') {
|
|
112
|
+
const declared = (op.tables.outputs?.rows ?? []).filter((r) => exchangeOf(unquote(r.file)) === response.awaiting?.exchange);
|
|
113
|
+
if (!declared.length) errors.push(`${rel('response/response.json')}: awaiting exchange ${response.awaiting?.exchange} is declared by no Output of ${op.id}`);
|
|
114
|
+
else if (!declared.some((r) => kindOf(r.kind) === response.awaiting?.kind)) errors.push(`${rel('response/response.json')}: awaiting kind ${response.awaiting?.kind} is not produced by exchange ${response.awaiting?.exchange}`);
|
|
115
|
+
}
|
|
116
|
+
const mainMd = outputs.find((r) => /\/response\.md$/.test(unquote(r.file)));
|
|
117
|
+
if (mainMd && present.has(kindOf(mainMd.kind))) {
|
|
118
|
+
const text = await readFile(path.join(dir, 'response', 'response.md'), 'utf8');
|
|
119
|
+
const taken = (tableUnder(text, '## Fallbacks taken') ?? []).map(([c]) => c);
|
|
120
|
+
const declaredTaken = new Set(response.fallbacks ?? []);
|
|
121
|
+
for (const c of taken) if (!declaredTaken.has(c)) errors.push(`${rel('response/response.md')}: Fallbacks taken lists ${c}, which response.json does not`);
|
|
122
|
+
for (const c of declaredTaken) if (!taken.includes(c)) errors.push(`${rel('response/response.json')}: fallback ${c} is not recorded under ## Fallbacks taken in response.md`);
|
|
123
|
+
}
|
|
124
|
+
for (const nextId of response.next ?? []) if (nextId !== 'user' && nextId !== 'external' && !packages.some((p) => p.manifest.id === nextId)) errors.push(`${rel('response/response.json')}: next names unknown operator ${nextId}`);
|
|
125
|
+
if (exchange && (response.next ?? []).length) errors.push(`${rel('response/response.json')}: a nested exchange does not route; next must be empty`);
|
|
126
|
+
return { errors, response, present, pkg };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
130
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
131
|
+
const target = process.argv[2];
|
|
132
|
+
if (!target) { process.stderr.write('usage: node scripts/validate-response.mjs <session>/step-N/parallel-M[/<exchange>]\n'); process.exit(2); }
|
|
133
|
+
const dir = path.resolve(target);
|
|
134
|
+
// A branch is <session>/step-N/parallel-M (its parent is step-N); an exchange is <branch>/<exchange> (its parent is parallel-M).
|
|
135
|
+
const exchange = /^step-\d+$/.test(path.basename(path.dirname(dir))) ? null : path.basename(dir);
|
|
136
|
+
let requirements = {};
|
|
137
|
+
const reqFile = path.join(exchange ? path.dirname(dir) : dir, 'request', 'request.json');
|
|
138
|
+
if (existsSync(reqFile)) requirements = JSON.parse(await readFile(reqFile, 'utf8')).requirements ?? {};
|
|
139
|
+
const { errors } = await validateResponse(root, dir, { requirements, exchange });
|
|
140
|
+
if (errors.length) { process.stderr.write(`${errors.join('\n')}\n`); process.exitCode = 1; } else process.stdout.write('response valid\n');
|
|
141
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import process from 'node:process';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { parseOperatorMd, cellCodes } from './operator-md.mjs';
|
|
7
|
+
import { loadErrorsRegistry } from './errors-registry.mjs';
|
|
8
|
+
|
|
9
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
10
|
+
const operatorsDir = path.join(root, 'operators');
|
|
11
|
+
const routing = JSON.parse(await readFile(path.join(root, 'routing.json'), 'utf8'));
|
|
12
|
+
const errors = [];
|
|
13
|
+
|
|
14
|
+
const kinds = new Set(Object.keys(routing.kinds));
|
|
15
|
+
const operators = new Map();
|
|
16
|
+
const registry = await loadErrorsRegistry(root);
|
|
17
|
+
errors.push(...registry.errors);
|
|
18
|
+
|
|
19
|
+
for (const entry of await readdir(operatorsDir, { withFileTypes: true })) {
|
|
20
|
+
if (!entry.isDirectory()) continue;
|
|
21
|
+
const dir = path.join(operatorsDir, entry.name);
|
|
22
|
+
const manifest = JSON.parse(await readFile(path.join(dir, 'operator.json'), 'utf8'));
|
|
23
|
+
if (existsSync(path.join(dir, 'operator.md'))) {
|
|
24
|
+
// An operator.md package emits exactly the domains of the stop codes its Stops table lists; `self`
|
|
25
|
+
// is the operator's own domain, which routing.json answers with kind "resume".
|
|
26
|
+
const op = parseOperatorMd(await readFile(path.join(dir, 'operator.md'), 'utf8'), 'en');
|
|
27
|
+
const domains = new Set();
|
|
28
|
+
for (const row of op.tables.stops?.rows ?? []) {
|
|
29
|
+
const code = cellCodes(row.code)[0] ?? row.code.trim();
|
|
30
|
+
const domain = registry.codes[code]?.domain;
|
|
31
|
+
if (!domain) continue; // validate-operator reports the unknown code
|
|
32
|
+
domains.add(domain === 'self' ? manifest.domain : domain);
|
|
33
|
+
}
|
|
34
|
+
operators.set(manifest.id, domains.size ? domains : null);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
const schema = await readFile(path.join(dir, 'output.schema.json'), 'utf8');
|
|
38
|
+
const match = /"owningDomain"\s*:\s*\{[^}]*"enum"\s*:\s*(\[[^\]]*\])/s.exec(schema);
|
|
39
|
+
operators.set(manifest.id, match ? new Set(JSON.parse(match[1])) : null);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Every operator must have a route table, and the table must cover exactly the domains
|
|
43
|
+
// that operator can actually emit. A domain it can emit but cannot route is a dead end;
|
|
44
|
+
// a route for a domain it never emits is a rule nobody reaches.
|
|
45
|
+
for (const [id, domains] of operators) {
|
|
46
|
+
const table = routing.routes[id];
|
|
47
|
+
if (!table) {
|
|
48
|
+
errors.push(`${id}: no route table`);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (domains === null) {
|
|
52
|
+
errors.push(`${id}: output schema declares no owningDomain enum`);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
for (const domain of domains) {
|
|
56
|
+
if (!Object.hasOwn(table, domain)) errors.push(`${id}: emits "${domain}" with no route`);
|
|
57
|
+
}
|
|
58
|
+
for (const domain of Object.keys(table)) {
|
|
59
|
+
if (!domains.has(domain)) errors.push(`${id}: routes "${domain}" which it never emits`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
for (const id of Object.keys(routing.routes)) {
|
|
64
|
+
if (!operators.has(id)) errors.push(`routing.json names unknown operator ${id}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// A destination must resolve. An "operator" route needs a real target; the other kinds
|
|
68
|
+
// must not carry one, or the map would silently disagree with itself.
|
|
69
|
+
for (const [id, table] of Object.entries(routing.routes)) {
|
|
70
|
+
for (const [domain, route] of Object.entries(table)) {
|
|
71
|
+
if (!kinds.has(route.kind)) {
|
|
72
|
+
errors.push(`${id}.${domain}: unknown kind ${route.kind}`);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (route.kind === 'operator') {
|
|
76
|
+
if (!route.target) errors.push(`${id}.${domain}: operator route needs a target`);
|
|
77
|
+
else if (!operators.has(route.target)) errors.push(`${id}.${domain}: targets unknown operator ${route.target}`);
|
|
78
|
+
else if (route.target === id) errors.push(`${id}.${domain}: targets itself; use kind "resume"`);
|
|
79
|
+
} else if (route.target !== undefined) {
|
|
80
|
+
errors.push(`${id}.${domain}: kind ${route.kind} must not carry a target`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (errors.length > 0) {
|
|
86
|
+
process.stderr.write(`${errors.join('\n')}\n`);
|
|
87
|
+
process.exitCode = 1;
|
|
88
|
+
} else {
|
|
89
|
+
const routes = Object.values(routing.routes).reduce((total, table) => total + Object.keys(table).length, 0);
|
|
90
|
+
process.stdout.write(`routing map closed: ${operators.size} operators, ${routes} routes\n`);
|
|
91
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// One branch of one step, both halves: validate-request on request/request.json, validate-response on
|
|
2
|
+
// response/, and the same pair on every nested exchange folder the response awaited or the operator
|
|
3
|
+
// declares. Used by operator self-tests and audits; the orchestrator runs the halves separately, the
|
|
4
|
+
// request before it spawns the agent and the response after.
|
|
5
|
+
import { existsSync } from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import process from 'node:process';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
import { validateRequest } from './validate-request.mjs';
|
|
10
|
+
import { validateResponse } from './validate-response.mjs';
|
|
11
|
+
import { loadOperatorPackages, exchangeOf } from './operator-md.mjs';
|
|
12
|
+
import { loadKindTemplates } from './validate-templates.mjs';
|
|
13
|
+
import { loadErrorsRegistry } from './errors-registry.mjs';
|
|
14
|
+
|
|
15
|
+
const unquote = (s) => String(s ?? '').trim().replace(/^`|`$/g, '');
|
|
16
|
+
|
|
17
|
+
export async function validateStep(root, branchDir) {
|
|
18
|
+
const packages = await loadOperatorPackages(root);
|
|
19
|
+
const kinds = await loadKindTemplates(root);
|
|
20
|
+
const registry = await loadErrorsRegistry(root);
|
|
21
|
+
const req = await validateRequest(root, branchDir, packages);
|
|
22
|
+
const errors = [...req.errors];
|
|
23
|
+
const requirements = req.request?.requirements ?? {};
|
|
24
|
+
const res = await validateResponse(root, branchDir, { requirements, exchange: null, packages, kinds, registry });
|
|
25
|
+
errors.push(...res.errors);
|
|
26
|
+
const present = new Set(res.present);
|
|
27
|
+
const pkg = req.pkg ?? res.pkg;
|
|
28
|
+
if (pkg?.shape === 'v9') {
|
|
29
|
+
const exchanges = new Set((pkg.en.tables.outputs?.rows ?? []).map((r) => exchangeOf(unquote(r.file))).filter(Boolean));
|
|
30
|
+
for (const ex of exchanges) {
|
|
31
|
+
const exDir = path.join(branchDir, ex);
|
|
32
|
+
if (!existsSync(path.join(exDir, 'request', 'request.json'))) { if (res.response?.status === 'done') errors.push(`${ex}/: the operator declares this exchange and the branch is done, but it never ran`); continue; }
|
|
33
|
+
const exReq = await validateRequest(root, exDir, packages);
|
|
34
|
+
errors.push(...exReq.errors);
|
|
35
|
+
const exRes = await validateResponse(root, exDir, { requirements, exchange: ex, packages, kinds, registry });
|
|
36
|
+
errors.push(...exRes.errors);
|
|
37
|
+
for (const k of exRes.present) present.add(k);
|
|
38
|
+
if (res.response?.status === 'done' && exRes.response?.status !== 'done') errors.push(`${ex}/response/response.json: the branch is done but the exchange is ${exRes.response?.status ?? 'missing'}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return { errors, request: req.request, response: res.response, requirements, present, pkg };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
45
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
46
|
+
const target = process.argv[2];
|
|
47
|
+
if (!target) { process.stderr.write('usage: node scripts/validate-step.mjs <session>/step-N/parallel-M\n'); process.exit(2); }
|
|
48
|
+
const { errors } = await validateStep(root, path.resolve(target));
|
|
49
|
+
if (errors.length) { process.stderr.write(`${errors.join('\n')}\n`); process.exitCode = 1; } else process.stdout.write('step valid\n');
|
|
50
|
+
}
|