@kontourai/flow-agents 2.2.0 → 2.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/.github/CODEOWNERS +8 -0
- package/.github/workflows/ci.yml +32 -0
- package/AGENTS.md +4 -4
- package/CHANGELOG.md +46 -0
- package/CONTEXT.md +14 -0
- package/README.md +9 -3
- package/agents/dev.json +1 -1
- package/agents/tool-code-reviewer.json +1 -1
- package/agents/tool-planner.json +3 -3
- package/agents/tool-verifier.json +3 -3
- package/build/src/cli/console-learning-projection.js +3 -2
- package/build/src/cli/init.js +104 -21
- package/build/src/cli/kit.js +37 -6
- package/build/src/cli/pull-work-provider.js +1 -1
- package/build/src/cli/usage-feedback.js +3 -3
- package/build/src/cli/validate-hook-influence.js +1 -0
- package/build/src/cli/validate-workflow-artifacts.js +22 -6
- package/build/src/cli/workflow-artifact-cleanup-audit.js +36 -2
- package/build/src/cli/workflow-sidecar.d.ts +14 -7
- package/build/src/cli/workflow-sidecar.js +792 -69
- package/build/src/flow-kit/validate.d.ts +23 -0
- package/build/src/flow-kit/validate.js +67 -1
- package/build/src/index.d.ts +1 -1
- package/build/src/index.js +1 -1
- package/build/src/lib/flow-resolver.d.ts +6 -4
- package/build/src/lib/flow-resolver.js +125 -58
- package/build/src/lib/local-artifact-root.d.ts +14 -2
- package/build/src/lib/local-artifact-root.js +22 -5
- package/build/src/lib/workflow-learning-projection.js +2 -2
- package/build/src/runtime-adapters.d.ts +12 -0
- package/build/src/runtime-adapters.js +56 -16
- package/build/src/tools/build-universal-bundles.js +49 -16
- package/build/src/tools/generate-context-map.js +10 -8
- package/build/src/tools/validate-source-tree.js +23 -1
- package/console.telemetry.json +22 -0
- package/context/contracts/artifact-contract.md +38 -7
- package/context/contracts/builder-kit-workflow-state-contract.md +3 -2
- package/context/contracts/decision-registry-contract.md +263 -0
- package/context/contracts/delivery-contract.md +1 -1
- package/context/contracts/planning-contract.md +2 -1
- package/context/contracts/review-contract.md +5 -3
- package/context/contracts/verification-contract.md +9 -1
- package/context/contracts/work-item-contract.md +1 -1
- package/context/deferred/parallelization.md +1 -1
- package/context/gate-awareness.md +4 -4
- package/context/scripts/git-status.sh +2 -2
- package/context/scripts/hooks/config-protection.js +502 -2
- package/context/scripts/hooks/lib/config-protection-remedies.js +69 -0
- package/context/scripts/hooks/run-hook.js +14 -1
- package/context/scripts/hooks/stop-goal-fit.js +1140 -99
- package/context/scripts/hooks/workflow-steering.js +212 -6
- package/docs/adr/0007-flow-skill-kit-tool-boundary.md +2 -0
- package/docs/adr/0015-flow-flow-agents-boundary-reconciliation.md +35 -0
- package/docs/adr/0019-kit-dependency-ownership.md +62 -0
- package/docs/adr/0020-trust-reconcile-manifest-and-claim-classification.md +290 -0
- package/docs/adr/0021-assignment-leases-and-stale-claim-takeover.md +221 -0
- package/docs/adr/0022-fail-closed-delivery-reconciliation-with-governed-exemptions.md +283 -0
- package/docs/agent-system-guidebook.md +3 -3
- package/docs/agent-usage-feedback-loop.md +6 -6
- package/docs/context-map.md +9 -7
- package/docs/decisions/decision-records.md +57 -0
- package/docs/decisions/index.md +14 -0
- package/docs/decisions/promotion-gate.md +52 -0
- package/docs/developer-architecture.md +1 -1
- package/docs/fixture-ownership.md +3 -0
- package/docs/flow-kit-repository-contract.md +5 -5
- package/docs/getting-started.md +12 -2
- package/docs/integrations/harness-install.md +2 -1
- package/docs/kit-authoring-guide.md +64 -5
- package/docs/knowledge-kit.md +5 -1
- package/docs/learnings/2026-07-improvement-program.md +203 -0
- package/docs/migrations.md +6 -5
- package/docs/north-star.md +2 -2
- package/docs/operating-layers.md +4 -3
- package/docs/repository-structure.md +5 -5
- package/docs/skills-map.md +8 -8
- package/docs/spec/runtime-hook-surface.md +19 -15
- package/docs/spikes/graph-provider-2026-07.md +155 -0
- package/docs/standards-register.md +7 -7
- package/docs/trust-anchor-adoption.md +105 -4
- package/docs/work-item-adapters.md +1 -1
- package/docs/workflow-artifact-lifecycle.md +72 -9
- package/docs/workflow-eval-strategy.md +3 -3
- package/docs/workflow-shared-contracts.md +1 -1
- package/docs/workflow-usage-guide.md +254 -21
- package/evals/acceptance/DEMO-false-completion.md +20 -11
- package/evals/acceptance/prove-capture-teeth-declared.sh +15 -15
- package/evals/acceptance/prove-capture-teeth.sh +11 -11
- package/evals/acceptance/test_kiro_harness.sh +1 -1
- package/evals/ci/antigaming-suite.sh +5 -0
- package/evals/ci/run-baseline.sh +65 -1
- package/evals/fixtures/flow-kit-repository/invalid-bad-dependency/flows/review.flow.json +26 -0
- package/evals/fixtures/flow-kit-repository/invalid-bad-dependency/kit.json +13 -0
- package/evals/fixtures/flow-kit-repository/valid-with-dependency/flows/review.flow.json +26 -0
- package/evals/fixtures/flow-kit-repository/valid-with-dependency/kit.json +13 -0
- package/evals/fixtures/hook-influence/cases.json +32 -0
- package/evals/fixtures/pull-work-provider/github-issues.json +6 -6
- package/evals/fixtures/trust-reconcile-exploits/fabricated-attestation.json +59 -0
- package/evals/fixtures/trust-reconcile-exploits/no-label-bypass.json +36 -0
- package/evals/fixtures/trust-reconcile-exploits/skip-assumed-bypass.json +59 -0
- package/evals/fixtures/trust-reconcile-exploits/status-misassertion.json +48 -0
- package/evals/fixtures/trust-reconcile-exploits/waived-command-check.json +44 -0
- package/evals/fixtures/trust-reconcile-mixed-bundle/mixed-bundle.json +159 -0
- package/evals/fixtures/trust-reconcile-ws3/ws3-bundle.json +920 -0
- package/evals/integration/test_actor_identity.sh +254 -0
- package/evals/integration/test_builder_step_producers.sh +33 -23
- package/evals/integration/test_bundle_install.sh +22 -16
- package/evals/integration/test_bundle_lifecycle.sh +11 -11
- package/evals/integration/test_captured_fail_reconciliation.sh +50 -50
- package/evals/integration/test_checkpoint_signing.sh +43 -0
- package/evals/integration/test_claim_lookup.sh +4 -4
- package/evals/integration/test_codex_hook_resolution.sh +114 -0
- package/evals/integration/test_command_log_concurrency.sh +8 -8
- package/evals/integration/test_command_log_fork_classification.sh +3 -3
- package/evals/integration/test_command_log_integrity.sh +16 -16
- package/evals/integration/test_console_learning_projection.sh +1 -1
- package/evals/integration/test_dual_emit_flow_step.sh +145 -0
- package/evals/integration/test_enforcer_expects_driven.sh +12 -12
- package/evals/integration/test_evidence_capture_hook.sh +14 -13
- package/evals/integration/test_fixture_retirement_audit.sh +2 -2
- package/evals/integration/test_flow_agents_statusline.sh +2 -2
- package/evals/integration/test_flow_kit_install_git.sh +52 -0
- package/evals/integration/test_flow_kit_repository.sh +2 -0
- package/evals/integration/test_flowdef_session_activation.sh +6 -6
- package/evals/integration/test_flowdef_union_floor_regression.sh +429 -0
- package/evals/integration/test_gate_bypass_chain.sh +52 -40
- package/evals/integration/test_gate_lockdown.sh +97 -73
- package/evals/integration/test_gate_review_inquiry_records.sh +3 -3
- package/evals/integration/test_goal_fit_escape_hatch.sh +10 -10
- package/evals/integration/test_goal_fit_ghost_session.sh +104 -0
- package/evals/integration/test_goal_fit_hook.sh +32 -32
- package/evals/integration/test_goal_fit_rederive.sh +9 -9
- package/evals/integration/test_hook_category_behaviors.sh +103 -0
- package/evals/integration/test_hook_influence_cases.sh +1 -0
- package/evals/integration/test_install_merge.sh +262 -1
- package/evals/integration/test_kit_identity_trust.sh +6 -6
- package/evals/integration/test_liveness_conflict_injection.sh +587 -0
- package/evals/integration/test_liveness_heartbeat.sh +677 -0
- package/evals/integration/test_liveness_verdict.sh +394 -0
- package/evals/integration/test_local_flow_kit_install.sh +15 -0
- package/evals/integration/test_phase_map_and_gate_claim.sh +63 -11
- package/evals/integration/test_promote_gate.sh +178 -0
- package/evals/integration/test_publish_delivery.sh +2 -2
- package/evals/integration/test_pull_work_liveness_preflight.sh +386 -0
- package/evals/integration/test_pull_work_provider.sh +2 -2
- package/evals/integration/test_reconcile_soundness.sh +4 -4
- package/evals/integration/test_resolvefirststep_security.sh +22 -0
- package/evals/integration/test_runtime_adapter_activation.sh +87 -18
- package/evals/integration/test_session_resume_roundtrip.sh +147 -0
- package/evals/integration/test_sidecar_field_preservation.sh +249 -0
- package/evals/integration/test_trust_reconcile.sh +2 -2
- package/evals/integration/test_trust_reconcile_manifest.sh +79 -0
- package/evals/integration/test_trust_reconcile_mixed_bundle.sh +96 -0
- package/evals/integration/test_trust_reconcile_negatives.sh +151 -0
- package/evals/integration/test_usage_feedback_global.sh +5 -5
- package/evals/integration/test_validate_artifacts_portability.sh +60 -0
- package/evals/integration/test_verify_cli.sh +2 -2
- package/evals/integration/test_veritas_governance_kit.sh +117 -0
- package/evals/integration/test_workflow_artifact_cleanup_audit.sh +26 -1
- package/evals/integration/test_workflow_artifacts.sh +33 -0
- package/evals/integration/test_workflow_sidecar_writer.sh +723 -74
- package/evals/integration/test_workflow_steering_hook.sh +175 -5
- package/evals/lib/codex-provider.sh +1 -1
- package/evals/lib/node.sh +19 -1
- package/evals/run.sh +22 -0
- package/evals/static/test_decisions.sh +150 -0
- package/evals/static/test_flowdef_codeowners_coverage.sh +56 -0
- package/evals/static/test_package.sh +19 -10
- package/evals/static/test_universal_bundles.sh +48 -3
- package/evals/static/test_workflow_skills.sh +41 -8
- package/kits/builder/flows/build.flow.json +3 -74
- package/kits/builder/flows/publish-learn.flow.json +90 -0
- package/kits/builder/kit.json +11 -0
- package/kits/builder/skills/builder-shape/SKILL.md +1 -1
- package/kits/builder/skills/deliver/SKILL.md +13 -11
- package/kits/builder/skills/design-probe/SKILL.md +4 -4
- package/kits/builder/skills/evidence-gate/SKILL.md +26 -1
- package/kits/builder/skills/execute-plan/SKILL.md +2 -2
- package/kits/builder/skills/fix-bug/SKILL.md +2 -0
- package/kits/builder/skills/gate-review/SKILL.md +6 -6
- package/kits/builder/skills/idea-to-backlog/SKILL.md +1 -1
- package/kits/builder/skills/learning-review/SKILL.md +7 -7
- package/kits/builder/skills/pickup-probe/SKILL.md +5 -4
- package/kits/builder/skills/plan-work/SKILL.md +27 -5
- package/kits/builder/skills/pull-work/SKILL.md +80 -5
- package/kits/builder/skills/release-readiness/SKILL.md +4 -4
- package/kits/builder/skills/review-work/SKILL.md +24 -1
- package/kits/builder/skills/tdd-workflow/SKILL.md +2 -0
- package/kits/builder/skills/verify-work/SKILL.md +8 -1
- package/kits/catalog.json +6 -0
- package/kits/veritas-governance/adapter/readiness-to-trust-bundle.mjs +178 -0
- package/kits/veritas-governance/docs/README.md +75 -0
- package/kits/veritas-governance/fixtures/readiness/not-ready.readiness-report.json +4645 -0
- package/kits/veritas-governance/fixtures/readiness/ready.readiness-report.json +4403 -0
- package/kits/veritas-governance/flows/readiness-check.flow.json +35 -0
- package/kits/veritas-governance/kit.json +16 -0
- package/package.json +8 -4
- package/packaging/README.md +2 -2
- package/packaging/conformance/fixtures/config-protection--allow-read-state-json.json +20 -0
- package/packaging/conformance/fixtures/config-protection--block-write-state-json.json +20 -0
- package/packaging/conformance/fixtures/stop-goal-fit--block-bundle-disputed-claim.json +1 -1
- package/packaging/conformance/fixtures/stop-goal-fit--block-capture-contradicts-claimed-pass.json +1 -1
- package/packaging/manifest.json +9 -10
- package/schemas/decision-record.schema.json +109 -0
- package/schemas/workflow-evidence.schema.json +1 -1
- package/schemas/workflow-state.schema.json +5 -0
- package/scripts/README.md +8 -3
- package/scripts/check-content-boundary.cjs +1 -1
- package/scripts/check-decisions.cjs +356 -0
- package/scripts/ci/derive-claim-status.mjs +74 -0
- package/scripts/ci/trust-reconcile.js +436 -79
- package/scripts/git-status.sh +2 -2
- package/scripts/hooks/claude-telemetry-hook.js +37 -3
- package/scripts/hooks/codex-telemetry-hook.js +36 -2
- package/scripts/hooks/config-protection.js +102 -30
- package/scripts/hooks/evidence-capture.js +3 -3
- package/scripts/hooks/lib/actor-identity.js +368 -0
- package/scripts/hooks/lib/config-protection-remedies.js +69 -0
- package/scripts/hooks/lib/liveness-heartbeat.js +361 -0
- package/scripts/hooks/lib/liveness-policy.js +127 -0
- package/scripts/hooks/lib/liveness-read.js +105 -12
- package/scripts/hooks/lib/liveness-write.js +46 -0
- package/scripts/hooks/lib/local-artifact-paths.js +12 -6
- package/scripts/hooks/opencode-telemetry-hook.js +27 -1
- package/scripts/hooks/pi-telemetry-hook.js +27 -1
- package/scripts/hooks/stop-goal-fit.js +157 -26
- package/scripts/hooks/workflow-steering.js +77 -3
- package/scripts/install-codex-home.sh +138 -38
- package/scripts/statusline/flow-agents-statusline.js +2 -2
- package/src/cli/console-learning-projection.ts +3 -2
- package/src/cli/init.ts +101 -21
- package/src/cli/kit.ts +37 -6
- package/src/cli/public-api.test.mjs +58 -6
- package/src/cli/pull-work-provider.ts +1 -1
- package/src/cli/trust-bundle-policy-order.test.mjs +87 -0
- package/src/cli/usage-feedback.ts +3 -3
- package/src/cli/validate-hook-influence.ts +1 -0
- package/src/cli/validate-workflow-artifacts.ts +22 -6
- package/src/cli/veritas-readiness-adapter.test.mjs +267 -0
- package/src/cli/workflow-artifact-cleanup-audit.ts +35 -2
- package/src/cli/workflow-sidecar.ts +795 -71
- package/src/flow-kit/validate.ts +74 -1
- package/src/index.ts +6 -2
- package/src/lib/flow-resolver.ts +123 -55
- package/src/lib/local-artifact-root.ts +24 -5
- package/src/lib/workflow-learning-projection.ts +2 -2
- package/src/runtime-adapters.ts +61 -15
- package/src/tools/build-universal-bundles.ts +49 -16
- package/src/tools/generate-context-map.ts +10 -8
- package/src/tools/validate-source-tree.ts +23 -1
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
// check-decisions.cjs — validator + index generator for the topic-keyed
|
|
5
|
+
// decision registry (context/contracts/decision-registry-contract.md).
|
|
6
|
+
//
|
|
7
|
+
// Usage:
|
|
8
|
+
// node scripts/check-decisions.cjs check # validate all topic files + assert index is current (default)
|
|
9
|
+
// node scripts/check-decisions.cjs gen-index # (re)write docs/decisions/index.md deterministically
|
|
10
|
+
//
|
|
11
|
+
// Zero runtime dependencies (mirrors scripts/check-content-boundary.cjs): the
|
|
12
|
+
// decision-record JSON schema at schemas/decision-record.schema.json is the
|
|
13
|
+
// normative structural contract; this script enforces the same rules directly
|
|
14
|
+
// so it runs in `npm ci` environments without a JSON-schema/YAML library.
|
|
15
|
+
|
|
16
|
+
const fs = require("node:fs");
|
|
17
|
+
const path = require("node:path");
|
|
18
|
+
|
|
19
|
+
const ROOT = path.resolve(__dirname, "..");
|
|
20
|
+
// FLOW_AGENTS_DECISIONS_DIR lets the eval suite point the validator/generator at
|
|
21
|
+
// a throwaway fixture directory (mirrors FLOW_AGENTS_CONTENT_BOUNDARY_FILES).
|
|
22
|
+
const DECISIONS_DIR = process.env.FLOW_AGENTS_DECISIONS_DIR
|
|
23
|
+
? path.resolve(process.env.FLOW_AGENTS_DECISIONS_DIR)
|
|
24
|
+
: path.join(ROOT, "docs", "decisions");
|
|
25
|
+
const INDEX_PATH = path.join(DECISIONS_DIR, "index.md");
|
|
26
|
+
const INDEX_SLUG = "index";
|
|
27
|
+
|
|
28
|
+
const STATUSES = ["current", "superseded", "merged", "needs-decision"];
|
|
29
|
+
const EVIDENCE_KINDS = ["issue", "pr", "commit", "session-archive", "adr", "doc", "url"];
|
|
30
|
+
const SLUG_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
31
|
+
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
32
|
+
|
|
33
|
+
// Secret-shaped literal detectors — mirror scripts/hooks/lib/patterns.sh so the
|
|
34
|
+
// registry never captures a credential in an evidence ref.
|
|
35
|
+
const SECRET_PATTERNS = [
|
|
36
|
+
{ label: "aws access key id", re: /AKIA[A-Z0-9]{16}/ },
|
|
37
|
+
{ label: "aws sts key id", re: /ASIA[A-Z0-9]{16}/ },
|
|
38
|
+
{ label: "github token", re: /gh[pousr]_[A-Za-z0-9_]{36,}/ },
|
|
39
|
+
{ label: "private key block", re: /BEGIN[A-Z ]*PRIVATE KEY/ },
|
|
40
|
+
{ label: "jwt", re: /eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/ },
|
|
41
|
+
{
|
|
42
|
+
label: "generic secret literal",
|
|
43
|
+
re: /(secret|password|token|api[_-]?key)\s*[:=]\s*["'][^"']{8,}/i,
|
|
44
|
+
},
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
function listTopicFiles() {
|
|
48
|
+
if (!fs.existsSync(DECISIONS_DIR)) return [];
|
|
49
|
+
return fs
|
|
50
|
+
.readdirSync(DECISIONS_DIR)
|
|
51
|
+
.filter((name) => name.endsWith(".md") && name !== "index.md")
|
|
52
|
+
.sort();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Minimal, strict YAML-frontmatter reader. The decision-record frontmatter is a
|
|
56
|
+
// flat map of scalars plus two list shapes (evidence[] as `- kind: .. ` blocks
|
|
57
|
+
// and slug arrays as inline `[a, b]` or block `- slug`). We parse exactly that
|
|
58
|
+
// shape and reject anything we do not understand rather than guessing.
|
|
59
|
+
function parseFrontmatter(raw, errors) {
|
|
60
|
+
if (!raw.startsWith("---\n") && !raw.startsWith("---\r\n")) {
|
|
61
|
+
errors.push("file must begin with a YAML frontmatter block delimited by '---'");
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
const lines = raw.split(/\r?\n/);
|
|
65
|
+
let end = -1;
|
|
66
|
+
for (let i = 1; i < lines.length; i += 1) {
|
|
67
|
+
if (lines[i].trim() === "---") {
|
|
68
|
+
end = i;
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (end === -1) {
|
|
73
|
+
errors.push("frontmatter block is not terminated by a closing '---'");
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
const body = lines.slice(1, end);
|
|
77
|
+
const data = {};
|
|
78
|
+
let cursorKey = null;
|
|
79
|
+
|
|
80
|
+
for (let i = 0; i < body.length; i += 1) {
|
|
81
|
+
const line = body[i];
|
|
82
|
+
if (line.trim() === "" || line.trim().startsWith("#")) continue;
|
|
83
|
+
|
|
84
|
+
const topMatch = line.match(/^([A-Za-z0-9_]+):\s*(.*)$/);
|
|
85
|
+
const listItem = line.match(/^\s*-\s+(.*)$/);
|
|
86
|
+
|
|
87
|
+
if (topMatch && !line.startsWith(" ")) {
|
|
88
|
+
const key = topMatch[1];
|
|
89
|
+
const rest = topMatch[2].trim();
|
|
90
|
+
cursorKey = key;
|
|
91
|
+
if (rest === "") {
|
|
92
|
+
data[key] = { __pending_list: true, items: [] };
|
|
93
|
+
} else if (rest.startsWith("[") && rest.endsWith("]")) {
|
|
94
|
+
const inner = rest.slice(1, -1).trim();
|
|
95
|
+
data[key] = inner === "" ? [] : inner.split(",").map((s) => stripScalar(s.trim()));
|
|
96
|
+
} else {
|
|
97
|
+
data[key] = stripScalar(rest);
|
|
98
|
+
}
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (listItem && cursorKey) {
|
|
103
|
+
const container = data[cursorKey];
|
|
104
|
+
if (!container || !container.__pending_list) {
|
|
105
|
+
errors.push(`unexpected list item under '${cursorKey}': ${line.trim()}`);
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
const itemBody = listItem[1].trim();
|
|
109
|
+
// Object list item: `- kind: pr, ref: ...` OR `- kind: pr` then following ` ref: ...`
|
|
110
|
+
const inlinePair = itemBody.match(/^([A-Za-z0-9_]+):\s*(.*)$/);
|
|
111
|
+
if (inlinePair) {
|
|
112
|
+
const obj = {};
|
|
113
|
+
obj[inlinePair[1]] = stripScalar(inlinePair[2].trim());
|
|
114
|
+
// consume subsequent indented `key: value` continuation lines
|
|
115
|
+
while (i + 1 < body.length && /^\s+[A-Za-z0-9_]+:\s*/.test(body[i + 1]) && !/^\s*-\s+/.test(body[i + 1])) {
|
|
116
|
+
const cont = body[i + 1].trim().match(/^([A-Za-z0-9_]+):\s*(.*)$/);
|
|
117
|
+
if (!cont) break;
|
|
118
|
+
obj[cont[1]] = stripScalar(cont[2].trim());
|
|
119
|
+
i += 1;
|
|
120
|
+
}
|
|
121
|
+
container.items.push(obj);
|
|
122
|
+
} else {
|
|
123
|
+
container.items.push(stripScalar(itemBody));
|
|
124
|
+
}
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
errors.push(`unparseable frontmatter line: ${line}`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Normalize pending lists to plain arrays.
|
|
132
|
+
for (const key of Object.keys(data)) {
|
|
133
|
+
if (data[key] && data[key].__pending_list) {
|
|
134
|
+
data[key] = data[key].items;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return data;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function stripScalar(value) {
|
|
141
|
+
let v = value.trim();
|
|
142
|
+
if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) {
|
|
143
|
+
v = v.slice(1, -1);
|
|
144
|
+
}
|
|
145
|
+
return v;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function firstBodyLine(raw) {
|
|
149
|
+
const lines = raw.split(/\r?\n/);
|
|
150
|
+
let seenClose = false;
|
|
151
|
+
let count = 0;
|
|
152
|
+
for (const line of lines) {
|
|
153
|
+
if (line.trim() === "---") {
|
|
154
|
+
count += 1;
|
|
155
|
+
if (count === 2) seenClose = true;
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (!seenClose) continue;
|
|
159
|
+
const t = line.trim();
|
|
160
|
+
if (t === "") continue;
|
|
161
|
+
return t.replace(/^#+\s*/, "").trim();
|
|
162
|
+
}
|
|
163
|
+
return "";
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function validateFile(slug, raw, knownSlugs) {
|
|
167
|
+
const errors = [];
|
|
168
|
+
const fm = parseFrontmatter(raw, errors);
|
|
169
|
+
if (!fm) return errors;
|
|
170
|
+
|
|
171
|
+
if (!SLUG_RE.test(slug)) {
|
|
172
|
+
errors.push(`filename slug '${slug}' is not a valid kebab-case topic slug`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// status
|
|
176
|
+
if (!("status" in fm)) {
|
|
177
|
+
errors.push("missing required field: status");
|
|
178
|
+
} else if (!STATUSES.includes(fm.status)) {
|
|
179
|
+
errors.push(`unknown status '${fm.status}' (allowed: ${STATUSES.join(", ")})`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// subject
|
|
183
|
+
if (!("subject" in fm) || typeof fm.subject !== "string" || fm.subject.trim() === "") {
|
|
184
|
+
errors.push("missing required field: subject (non-empty)");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// decided
|
|
188
|
+
if (!("decided" in fm)) {
|
|
189
|
+
errors.push("missing required field: decided");
|
|
190
|
+
} else if (typeof fm.decided !== "string" || !DATE_RE.test(fm.decided)) {
|
|
191
|
+
errors.push(`decided must be an ISO date (YYYY-MM-DD); got '${fm.decided}'`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// evidence
|
|
195
|
+
if (!("evidence" in fm)) {
|
|
196
|
+
errors.push("missing required field: evidence[]");
|
|
197
|
+
} else if (!Array.isArray(fm.evidence) || fm.evidence.length === 0) {
|
|
198
|
+
errors.push("evidence must be a non-empty array of {kind, ref}");
|
|
199
|
+
} else {
|
|
200
|
+
fm.evidence.forEach((item, idx) => {
|
|
201
|
+
if (typeof item !== "object" || item === null || Array.isArray(item)) {
|
|
202
|
+
errors.push(`evidence[${idx}] must be an object {kind, ref}`);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (!EVIDENCE_KINDS.includes(item.kind)) {
|
|
206
|
+
errors.push(`evidence[${idx}].kind '${item.kind}' is not one of ${EVIDENCE_KINDS.join(", ")}`);
|
|
207
|
+
}
|
|
208
|
+
if (typeof item.ref !== "string" || item.ref.trim() === "") {
|
|
209
|
+
errors.push(`evidence[${idx}].ref must be a non-empty string`);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
for (const pat of SECRET_PATTERNS) {
|
|
213
|
+
if (pat.re.test(item.ref)) {
|
|
214
|
+
errors.push(`evidence[${idx}].ref contains a secret-shaped literal (${pat.label}); link durable provenance, never a credential`);
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// tombstone / relationship fields
|
|
222
|
+
const relSingles = ["superseded_by", "merged_into"];
|
|
223
|
+
for (const key of relSingles) {
|
|
224
|
+
if (key in fm) {
|
|
225
|
+
const target = fm[key];
|
|
226
|
+
if (typeof target !== "string" || !SLUG_RE.test(target)) {
|
|
227
|
+
errors.push(`${key} must be a single topic slug; got '${target}'`);
|
|
228
|
+
} else if (!knownSlugs.has(target)) {
|
|
229
|
+
errors.push(`${key} points at missing topic slug '${target}' (no docs/decisions/${target}.md)`);
|
|
230
|
+
} else if (target === slug) {
|
|
231
|
+
errors.push(`${key} must not point at its own slug`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if ("supersedes" in fm) {
|
|
236
|
+
const arr = Array.isArray(fm.supersedes) ? fm.supersedes : [fm.supersedes];
|
|
237
|
+
arr.forEach((target) => {
|
|
238
|
+
if (typeof target !== "string" || !SLUG_RE.test(target)) {
|
|
239
|
+
errors.push(`supersedes entry must be a topic slug; got '${target}'`);
|
|
240
|
+
} else if (!knownSlugs.has(target)) {
|
|
241
|
+
errors.push(`supersedes points at missing topic slug '${target}'`);
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// status-conditional relationship rules
|
|
247
|
+
if (fm.status === "current" || fm.status === "needs-decision") {
|
|
248
|
+
if ("superseded_by" in fm || "merged_into" in fm) {
|
|
249
|
+
errors.push(`status '${fm.status}' must not carry superseded_by or merged_into`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
if (fm.status === "superseded") {
|
|
253
|
+
if (!("superseded_by" in fm)) errors.push("status 'superseded' requires superseded_by");
|
|
254
|
+
if ("merged_into" in fm) errors.push("status 'superseded' must not carry merged_into");
|
|
255
|
+
}
|
|
256
|
+
if (fm.status === "merged") {
|
|
257
|
+
if (!("merged_into" in fm)) errors.push("status 'merged' requires merged_into");
|
|
258
|
+
if ("superseded_by" in fm) errors.push("status 'merged' must not carry superseded_by");
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return errors;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function readTopics() {
|
|
265
|
+
const files = listTopicFiles();
|
|
266
|
+
const knownSlugs = new Set(files.map((f) => f.replace(/\.md$/, "")));
|
|
267
|
+
return files.map((file) => {
|
|
268
|
+
const slug = file.replace(/\.md$/, "");
|
|
269
|
+
const raw = fs.readFileSync(path.join(DECISIONS_DIR, file), "utf8");
|
|
270
|
+
const errors = validateFile(slug, raw, knownSlugs);
|
|
271
|
+
const fm = parseFrontmatter(raw, []);
|
|
272
|
+
return { slug, raw, fm: fm || {}, errors };
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function oneLiner(topic) {
|
|
277
|
+
const subject = topic.fm && typeof topic.fm.subject === "string" ? topic.fm.subject.trim() : "";
|
|
278
|
+
if (subject) return subject;
|
|
279
|
+
const body = firstBodyLine(topic.raw);
|
|
280
|
+
return body || topic.slug;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function renderIndex(topics) {
|
|
284
|
+
const lines = [];
|
|
285
|
+
lines.push("---");
|
|
286
|
+
lines.push("title: Decision Index");
|
|
287
|
+
lines.push("---");
|
|
288
|
+
lines.push("");
|
|
289
|
+
lines.push("# Decision Index");
|
|
290
|
+
lines.push("");
|
|
291
|
+
lines.push("Generated by `npm run gen:decisions-index`. Do not edit by hand.");
|
|
292
|
+
lines.push("Topic-keyed living decision records per `context/contracts/decision-registry-contract.md`.");
|
|
293
|
+
lines.push("Numbered ADRs under `docs/adr/` are frozen history and are not listed here.");
|
|
294
|
+
lines.push("");
|
|
295
|
+
lines.push("| Topic | Status | Decision |");
|
|
296
|
+
lines.push("| --- | --- | --- |");
|
|
297
|
+
const sorted = [...topics].sort((a, b) => a.slug.localeCompare(b.slug));
|
|
298
|
+
for (const topic of sorted) {
|
|
299
|
+
const status = topic.fm && typeof topic.fm.status === "string" ? topic.fm.status : "unknown";
|
|
300
|
+
const summary = oneLiner(topic).replace(/\|/g, "\\|");
|
|
301
|
+
lines.push(`| [${topic.slug}](./${topic.slug}.md) | ${status} | ${summary} |`);
|
|
302
|
+
}
|
|
303
|
+
lines.push("");
|
|
304
|
+
return lines.join("\n");
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function cmdGenIndex() {
|
|
308
|
+
fs.mkdirSync(DECISIONS_DIR, { recursive: true });
|
|
309
|
+
const topics = readTopics();
|
|
310
|
+
const content = renderIndex(topics);
|
|
311
|
+
fs.writeFileSync(INDEX_PATH, content, "utf8");
|
|
312
|
+
console.log(`Wrote ${path.relative(ROOT, INDEX_PATH)} (${topics.length} topic${topics.length === 1 ? "" : "s"}).`);
|
|
313
|
+
return 0;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function cmdCheck() {
|
|
317
|
+
const topics = readTopics();
|
|
318
|
+
let failed = false;
|
|
319
|
+
|
|
320
|
+
for (const topic of topics) {
|
|
321
|
+
if (topic.errors.length > 0) {
|
|
322
|
+
failed = true;
|
|
323
|
+
console.error(`FAIL docs/decisions/${topic.slug}.md`);
|
|
324
|
+
for (const err of topic.errors) console.error(` - ${err}`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Index freshness: check regenerates deterministically and must match on disk
|
|
329
|
+
// (this is what makes regeneration idempotent + diff-clean, AC2).
|
|
330
|
+
const expectedIndex = renderIndex(topics);
|
|
331
|
+
const actualIndex = fs.existsSync(INDEX_PATH) ? fs.readFileSync(INDEX_PATH, "utf8") : null;
|
|
332
|
+
if (actualIndex === null) {
|
|
333
|
+
failed = true;
|
|
334
|
+
console.error("FAIL docs/decisions/index.md is missing; run `npm run gen:decisions-index`");
|
|
335
|
+
} else if (actualIndex !== expectedIndex) {
|
|
336
|
+
failed = true;
|
|
337
|
+
console.error("FAIL docs/decisions/index.md is stale; run `npm run gen:decisions-index`");
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (failed) {
|
|
341
|
+
console.error("Decision registry check failed.");
|
|
342
|
+
return 1;
|
|
343
|
+
}
|
|
344
|
+
console.log(`Decision registry check passed (${topics.length} topic${topics.length === 1 ? "" : "s"}).`);
|
|
345
|
+
return 0;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function main() {
|
|
349
|
+
const mode = process.argv[2] || "check";
|
|
350
|
+
if (mode === "check") return process.exit(cmdCheck());
|
|
351
|
+
if (mode === "gen-index") return process.exit(cmdGenIndex());
|
|
352
|
+
console.error(`Unknown mode '${mode}'. Usage: check-decisions.cjs [check|gen-index]`);
|
|
353
|
+
return process.exit(2);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
main();
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// derive-claim-status.mjs — WS8 (AC1/finding-3) CI-side status re-derivation helper.
|
|
3
|
+
//
|
|
4
|
+
// The trust reconciler MUST NOT trust a bundle's self-reported `claim.status`. This helper
|
|
5
|
+
// re-derives each claim's status from the bundle's OWN evidence/events/policies using
|
|
6
|
+
// @kontourai/surface's canonical `deriveClaimStatus` — the exact function the producer used
|
|
7
|
+
// — so the reconciler can detect a status-misassertion (asserted status != derived status).
|
|
8
|
+
//
|
|
9
|
+
// It is a separate ESM module because @kontourai/surface is ESM-only while
|
|
10
|
+
// scripts/ci/trust-reconcile.js is CommonJS with a synchronous entrypoint; the reconciler
|
|
11
|
+
// invokes this via spawnSync and parses the JSON on stdout. Surface is resolved from THIS
|
|
12
|
+
// file's location (the reconciler's own node_modules), NOT the target repo-root, so an
|
|
13
|
+
// adopter repo without Surface still gets CI-side re-derivation from the anchor's copy.
|
|
14
|
+
//
|
|
15
|
+
// Usage: node derive-claim-status.mjs <bundle-path>
|
|
16
|
+
// Output (stdout): {"claimId": "<TrustStatus>", ...} — value is null if that claim threw.
|
|
17
|
+
// Exit: 0 on success (Surface loaded); 2 if Surface is unavailable; 3 on bad input.
|
|
18
|
+
|
|
19
|
+
import { readFileSync } from "node:fs";
|
|
20
|
+
|
|
21
|
+
async function main() {
|
|
22
|
+
const bundlePath = process.argv[2];
|
|
23
|
+
if (!bundlePath) { process.stderr.write("derive-claim-status: bundle path argument required\n"); return 3; }
|
|
24
|
+
|
|
25
|
+
let surface;
|
|
26
|
+
try {
|
|
27
|
+
surface = await import("@kontourai/surface");
|
|
28
|
+
} catch (err) {
|
|
29
|
+
process.stderr.write(`derive-claim-status: @kontourai/surface unavailable: ${err && err.message ? err.message : String(err)}\n`);
|
|
30
|
+
return 2;
|
|
31
|
+
}
|
|
32
|
+
const { deriveClaimStatus } = surface;
|
|
33
|
+
if (typeof deriveClaimStatus !== "function") {
|
|
34
|
+
process.stderr.write("derive-claim-status: @kontourai/surface deriveClaimStatus missing\n");
|
|
35
|
+
return 2;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let bundle;
|
|
39
|
+
try {
|
|
40
|
+
bundle = JSON.parse(readFileSync(bundlePath, "utf8"));
|
|
41
|
+
} catch (err) {
|
|
42
|
+
process.stderr.write(`derive-claim-status: cannot read bundle: ${err && err.message ? err.message : String(err)}\n`);
|
|
43
|
+
return 3;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const claims = Array.isArray(bundle.claims) ? bundle.claims : [];
|
|
47
|
+
const allEvidence = Array.isArray(bundle.evidence) ? bundle.evidence : [];
|
|
48
|
+
const allEvents = Array.isArray(bundle.events) ? bundle.events : [];
|
|
49
|
+
const allPolicies = Array.isArray(bundle.policies) ? bundle.policies : [];
|
|
50
|
+
|
|
51
|
+
const out = {};
|
|
52
|
+
for (const claim of claims) {
|
|
53
|
+
if (!claim || !claim.id) continue;
|
|
54
|
+
// Filter evidence by claimId (deriveTrustStatus does NOT filter evidence internally — it
|
|
55
|
+
// only filters events — so passing the whole array would let another claim's evidence
|
|
56
|
+
// type/blocking-failure bleed in). Events are filtered internally by claimId; policies are
|
|
57
|
+
// resolved per-claim via verificationPolicyId. This mirrors the producer's per-claim call.
|
|
58
|
+
const evidence = allEvidence.filter((e) => e && e.claimId === claim.id);
|
|
59
|
+
try {
|
|
60
|
+
const { status } = deriveClaimStatus({ claim, evidence, events: allEvents, policies: allPolicies });
|
|
61
|
+
out[claim.id] = status;
|
|
62
|
+
} catch {
|
|
63
|
+
out[claim.id] = null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
process.stdout.write(JSON.stringify(out));
|
|
68
|
+
return 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
main().then((code) => process.exit(code)).catch((err) => {
|
|
72
|
+
process.stderr.write(`derive-claim-status: ${err && err.message ? err.message : String(err)}\n`);
|
|
73
|
+
process.exit(2);
|
|
74
|
+
});
|