@haposoft/cafekit 0.16.7 → 0.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -22
- package/bin/__tests__/codex-hooks-ownership.test.js +2 -2
- package/bin/__tests__/codex-hooks.test.js +169 -272
- package/bin/__tests__/codex-native.test.js +100 -202
- package/bin/__tests__/develop-contract.test.js +122 -1413
- package/bin/__tests__/docs-sync-on-demand.test.js +93 -0
- package/bin/__tests__/grok-envelope.test.js +0 -56
- package/bin/__tests__/hook-payload.test.js +4 -6
- package/bin/__tests__/legacy-authority-decoupled.test.js +154 -0
- package/bin/__tests__/legacy-hooks-retired.test.js +221 -0
- package/bin/__tests__/omp-bridge.test.js +20 -17
- package/bin/__tests__/omp-hooks.test.js +0 -19
- package/bin/__tests__/optional-skill-inventory.test.js +112 -4
- package/bin/__tests__/orca-session.test.js +0 -2
- package/bin/__tests__/package-inventory.test.js +90 -925
- package/bin/__tests__/runtime-dir.test.js +0 -3
- package/bin/__tests__/runtime-schema.test.js +3 -2
- package/bin/__tests__/skill-routing-source.test.js +27 -15
- package/bin/__tests__/spec-narrowing.test.js +30 -161
- package/bin/__tests__/specs-v2-execution-closeout.test.js +0 -437
- package/bin/__tests__/test-runtime-dependency-closure.cjs +1 -2
- package/bin/__tests__/ui-ux-skill.test.js +101 -0
- package/bin/__tests__/usage-hook-retired.test.js +114 -0
- package/bin/__tests__/validator-grounding.test.js +18 -2949
- package/bin/lib/managed-writer.js +2 -3
- package/bin/phases/codex-runtime.js +27 -0
- package/bin/phases/copy-payload.js +13 -28
- package/bin/phases/omp-runtime.js +25 -0
- package/package.json +2 -3
- package/src/claude/CLAUDE.md +3 -10
- package/src/claude/agents/brainstormer.md +7 -3
- package/src/claude/agents/code-auditor.md +44 -56
- package/src/claude/agents/debugger.md +8 -88
- package/src/claude/agents/deployer.md +3 -5
- package/src/claude/agents/docs-keeper.md +6 -14
- package/src/claude/agents/git-ops.md +2 -4
- package/src/claude/agents/implementer.md +12 -16
- package/src/claude/agents/inspector.md +4 -3
- package/src/claude/agents/project-manager.md +9 -17
- package/src/claude/agents/researcher.md +9 -3
- package/src/claude/agents/spec-maker.md +15 -15
- package/src/claude/agents/test-runner.md +8 -11
- package/src/claude/agents/ui-ux-designer.md +0 -1
- package/src/claude/hooks/__tests__/fixtures/statusline-default.golden +1 -1
- package/src/claude/hooks/__tests__/spec-gate.test.js +244 -333
- package/src/claude/hooks/__tests__/spec-state-touched.test.js +160 -0
- package/src/claude/hooks/__tests__/state.test.js +68 -0
- package/src/claude/hooks/__tests__/statusline.test.js +140 -11
- package/src/claude/hooks/agent.cjs +2 -1
- package/src/claude/hooks/lib/hook-payload.cjs +2 -2
- package/src/claude/hooks/rules.cjs +2 -1
- package/src/claude/hooks/session.cjs +0 -1
- package/src/claude/hooks/spec-gate.cjs +17 -83
- package/src/claude/hooks/spec-state.cjs +142 -39
- package/src/claude/hooks/state.cjs +21 -2
- package/src/claude/migration-manifest.json +42 -22
- package/src/claude/output-styles/coding-level-0-eli5.md +9 -9
- package/src/claude/output-styles/coding-level-1-junior.md +3 -3
- package/src/claude/output-styles/coding-level-2-mid.md +3 -3
- package/src/claude/output-styles/coding-level-3-senior.md +2 -2
- package/src/claude/output-styles/coding-level-4-lead.md +5 -5
- package/src/claude/output-styles/coding-level-5-god.md +6 -6
- package/src/claude/rules/ai-dev-rules.md +2 -3
- package/src/claude/rules/hook-protocols.md +8 -15
- package/src/claude/rules/manage-docs.md +9 -23
- package/src/claude/rules/orchestrator.md +3 -4
- package/src/claude/rules/review-audit-self-decision.md +3 -3
- package/src/claude/rules/skill-domain-routing.md +1 -3
- package/src/claude/rules/skill-workflow-routing.md +4 -5
- package/src/claude/rules/state-sync.md +3 -12
- package/src/claude/rules/workflow.md +11 -19
- package/src/claude/runtime.schema.json +5 -3
- package/src/claude/scripts/generate-skill-catalog.cjs +7 -6
- package/src/claude/scripts/provenance.cjs +23 -5
- package/src/claude/scripts/spec-receipt.cjs +24 -121
- package/src/claude/scripts/spec-resolver.cjs +41 -458
- package/src/claude/scripts/workflow-policy.cjs +3 -1801
- package/src/claude/settings/settings.json +1 -32
- package/src/claude/skills/agent-browser/SKILL.md +1 -1
- package/src/claude/skills/{question → ask}/SKILL.md +9 -75
- package/src/claude/skills/brainstorm/SKILL.md +43 -131
- package/src/claude/skills/chrome-devtools/SKILL.md +14 -31
- package/src/claude/skills/code-review/SKILL.md +29 -81
- package/src/claude/skills/code-review/references/spec-compliance-review.md +4 -8
- package/src/claude/skills/debug/SKILL.md +29 -149
- package/src/claude/skills/develop/SKILL.md +26 -83
- package/src/claude/skills/develop/references/parallel-waves.md +0 -8
- package/src/claude/skills/develop/references/quality-gate.md +15 -13
- package/src/claude/skills/develop/references/subagent-patterns.md +0 -6
- package/src/claude/skills/docs/SKILL.md +2 -2
- package/src/claude/skills/docs/references/update-workflow.md +1 -1
- package/src/claude/skills/fix/SKILL.md +153 -0
- package/src/claude/skills/{hotfix → fix}/references/diagnosis-protocol.md +2 -2
- package/src/claude/skills/{hotfix → fix}/references/parallel-patterns.md +2 -2
- package/src/claude/skills/{hotfix → fix}/references/prevention-gate.md +3 -3
- package/src/claude/skills/{hotfix → fix}/references/review-cycle.md +1 -1
- package/src/claude/skills/git/SKILL.md +48 -9
- package/src/claude/skills/git/references/commit-protocols.md +35 -16
- package/src/claude/skills/git/references/worktree-blueprint.md +42 -6
- package/src/claude/skills/orca/SKILL.md +9 -8
- package/src/claude/skills/research/SKILL.md +17 -46
- package/src/claude/skills/scout/SKILL.md +61 -0
- package/src/claude/skills/{inspect → scout}/references/internal-inspection.md +26 -10
- package/src/claude/skills/specs/SKILL.md +22 -29
- package/src/claude/skills/specs/references/review.md +8 -6
- package/src/claude/skills/specs/references/templates.md +34 -41
- package/src/claude/skills/sync/SKILL.md +16 -45
- package/src/claude/skills/sync/references/rebind-and-audit.md +59 -0
- package/src/claude/skills/sync/references/sync-protocols.md +4 -13
- package/src/claude/skills/test/SKILL.md +39 -104
- package/src/claude/skills/test/references/execution-strategy.md +93 -21
- package/src/claude/skills/test/references/failure-triage.md +1 -1
- package/src/claude/skills/ui-ux/LICENSE +21 -0
- package/src/claude/skills/ui-ux/SKILL.md +497 -0
- package/src/claude/skills/ui-ux/references/brand-tokens.md +90 -0
- package/src/claude/skills/ui-ux/references/budgets.md +113 -0
- package/src/claude/skills/ui-ux/references/checklist.md +271 -0
- package/src/claude/skills/ui-ux/references/components/accordion.md +246 -0
- package/src/claude/skills/ui-ux/references/components/avatar.md +153 -0
- package/src/claude/skills/ui-ux/references/components/banner.md +43 -0
- package/src/claude/skills/ui-ux/references/components/breadcrumb.md +201 -0
- package/src/claude/skills/ui-ux/references/components/button.md +142 -0
- package/src/claude/skills/ui-ux/references/components/card.md +113 -0
- package/src/claude/skills/ui-ux/references/components/charts.md +376 -0
- package/src/claude/skills/ui-ux/references/components/chat.md +132 -0
- package/src/claude/skills/ui-ux/references/components/choice-controls.md +369 -0
- package/src/claude/skills/ui-ux/references/components/comment-thread.md +17 -0
- package/src/claude/skills/ui-ux/references/components/description-list.md +116 -0
- package/src/claude/skills/ui-ux/references/components/empty-state.md +98 -0
- package/src/claude/skills/ui-ux/references/components/file-upload.md +107 -0
- package/src/claude/skills/ui-ux/references/components/inline-edit.md +128 -0
- package/src/claude/skills/ui-ux/references/components/input.md +161 -0
- package/src/claude/skills/ui-ux/references/components/list-row.md +56 -0
- package/src/claude/skills/ui-ux/references/components/loading.md +198 -0
- package/src/claude/skills/ui-ux/references/components/logo.md +147 -0
- package/src/claude/skills/ui-ux/references/components/otp-input.md +43 -0
- package/src/claude/skills/ui-ux/references/components/quantity-input.md +100 -0
- package/src/claude/skills/ui-ux/references/components/range-slider.md +16 -0
- package/src/claude/skills/ui-ux/references/components/small-controls.md +340 -0
- package/src/claude/skills/ui-ux/references/components/sortable-header.md +86 -0
- package/src/claude/skills/ui-ux/references/components/tag-input.md +14 -0
- package/src/claude/skills/ui-ux/references/components/timeline.md +38 -0
- package/src/claude/skills/ui-ux/references/components/tree.md +30 -0
- package/src/claude/skills/ui-ux/references/design-process.md +650 -0
- package/src/claude/skills/ui-ux/references/layouts/app-kanban.html +303 -0
- package/src/claude/skills/ui-ux/references/layouts/app.md +1369 -0
- package/src/claude/skills/ui-ux/references/layouts/form.md +430 -0
- package/src/claude/skills/ui-ux/references/layouts/overlay.md +552 -0
- package/src/claude/skills/ui-ux/references/layouts/pricing.md +204 -0
- package/src/claude/skills/ui-ux/references/locked-rules.md +35 -0
- package/src/claude/skills/ui-ux/references/principles.md +408 -0
- package/src/claude/skills/ui-ux/references/refactor.md +245 -0
- package/src/claude/skills/ui-ux/references/responsive.md +195 -0
- package/src/claude/skills/ui-ux/references/review.md +528 -0
- package/src/claude/skills/ui-ux/references/rules-color.md +623 -0
- package/src/claude/skills/ui-ux/references/rules-form.md +235 -0
- package/src/claude/skills/ui-ux/references/rules-state.md +680 -0
- package/src/claude/skills/ui-ux/references/rules-type.md +369 -0
- package/src/claude/skills/ui-ux/references/styles.md +311 -0
- package/src/claude/skills/ui-ux/references/system.md +219 -0
- package/src/claude/skills/ui-ux/references/tailwind-v4-traps.md +237 -0
- package/src/claude/skills/ui-ux/references/tokens.css +362 -0
- package/src/claude/skills/ui-ux/scripts/lint-skill.mjs +216 -0
- package/src/claude/skills/ui-ux/scripts/package.json +9 -0
- package/src/claude/skills/ui-ux/scripts/probe.mjs +4447 -0
- package/src/claude/skills/ui-ux-pro-max/SKILL.md +25 -80
- package/src/claude/skills/web-testing/SKILL.md +2 -20
- package/src/claude/status.cjs +149 -144
- package/src/codex/AGENTS.md +8 -13
- package/src/codex/hooks/agent.cjs +2 -1
- package/src/codex/hooks/lib/spec-receipt.cjs +4 -32
- package/src/codex/hooks/lib/spec-utils.cjs +6 -52
- package/src/codex/hooks/rules.cjs +2 -1
- package/src/codex/hooks/session.cjs +0 -2
- package/src/codex/hooks/spec-gate.cjs +13 -81
- package/src/codex/hooks/spec-state.cjs +37 -41
- package/src/codex/hooks.json +0 -20
- package/src/codex/rules/state-sync.md +3 -12
- package/src/common/AGENTS.md +6 -12
- package/src/omp/extensions/cafekit-bridge.mjs +6 -6
- package/bin/__tests__/change-firewall.semantic-firewall.test.js +0 -2133
- package/bin/__tests__/finalizer-pass-gate.semantic-firewall.test.js +0 -342
- package/bin/__tests__/release-preflight.semantic-firewall.test.js +0 -586
- package/bin/__tests__/review-receipt-c2.semantic-firewall.test.js +0 -378
- package/bin/__tests__/specs-v2-policy-and-scaffold.test.js +0 -665
- package/bin/__tests__/specs-v2-validator-grounder-contract.test.js +0 -1132
- package/src/claude/hooks/__tests__/completion-authority.test.js +0 -990
- package/src/claude/hooks/__tests__/semantic-review-authority.test.js +0 -259
- package/src/claude/hooks/completion-authority-check.cjs +0 -56
- package/src/claude/hooks/completion-authority-state.cjs +0 -527
- package/src/claude/hooks/completion-authority.cjs +0 -163
- package/src/claude/hooks/lib/context.cjs +0 -616
- package/src/claude/hooks/lib/detect.cjs +0 -474
- package/src/claude/hooks/semantic-review-authority.cjs +0 -154
- package/src/claude/hooks/task-scaffold-guard.cjs +0 -114
- package/src/claude/hooks/usage.cjs +0 -197
- package/src/claude/scripts/change-firewall.cjs +0 -1748
- package/src/claude/scripts/spec-authoring-digest.cjs +0 -151
- package/src/claude/scripts/spec-authoring-validation.cjs +0 -126
- package/src/claude/scripts/spec-final-state.cjs +0 -290
- package/src/claude/scripts/spec-ground.cjs +0 -650
- package/src/claude/scripts/spec-readiness.cjs +0 -425
- package/src/claude/scripts/spec-scaffold.cjs +0 -2317
- package/src/claude/scripts/spec-semantic-model.cjs +0 -396
- package/src/claude/scripts/validate-spec-output.cjs +0 -3941
- package/src/claude/skills/code-review/references/verification-gate.md +0 -46
- package/src/claude/skills/delegate/SKILL.md +0 -124
- package/src/claude/skills/delegate/references/codex-delegation.md +0 -71
- package/src/claude/skills/delegate/references/grok-delegation.md +0 -84
- package/src/claude/skills/hotfix/SKILL.md +0 -289
- package/src/claude/skills/inspect/SKILL.md +0 -142
- package/src/claude/skills/loop/SKILL.md +0 -114
- package/src/claude/skills/loop/references/bounded-loop-protocol.md +0 -104
- package/src/claude/skills/loop/references/metric-and-guard-contract.md +0 -76
- package/src/claude/skills/route/SKILL.md +0 -94
- package/src/claude/skills/route/references/agent-timing.md +0 -46
- package/src/claude/skills/route/references/chaining-patterns.md +0 -53
- package/src/claude/skills/route/references/task-taxonomy.md +0 -48
- package/src/claude/skills/specs/templates/design.md +0 -82
- package/src/claude/skills/specs/templates/requirements-init.md +0 -8
- package/src/claude/skills/specs/templates/requirements.md +0 -42
- package/src/claude/skills/specs/templates/spec-state.json +0 -56
- package/src/claude/skills/specs/templates/task.md +0 -47
- package/src/codex/hooks/completion-authority-check.cjs +0 -67
- package/src/codex/hooks/completion-authority-state.cjs +0 -527
- package/src/codex/hooks/completion-authority.cjs +0 -109
- package/src/codex/hooks/semantic-review-authority.cjs +0 -130
- package/src/codex/hooks/task-scaffold-guard.cjs +0 -63
- /package/src/claude/skills/{question → ask}/templates/question.md +0 -0
- /package/src/claude/skills/{hotfix → fix}/references/escalation-tactics.md +0 -0
- /package/src/claude/skills/{hotfix → fix}/references/workflow-specialized.md +0 -0
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
// Shared C16 freshness primitive (D13). Owned by R0-01. This module is the sole
|
|
4
|
-
// place the raw-byte / stable-JSON digest algorithm for authoring artifacts is
|
|
5
|
-
// implemented; R1-01's spec-authoring-validation.cjs and validate-spec-output.cjs's
|
|
6
|
-
// authorized_evolution/finalizer freshness checks reuse it unmodified (I20).
|
|
7
|
-
//
|
|
8
|
-
// Non-circular: every digest here is computed only from an authored artifact's own
|
|
9
|
-
// bytes (requirements.md, design.md, research.md, task files). None of these ever
|
|
10
|
-
// hash semantic_digest, review_receipt_digest, or the C16 receipt's own bytes.
|
|
11
|
-
|
|
12
|
-
const crypto = require('node:crypto');
|
|
13
|
-
const fs = require('node:fs');
|
|
14
|
-
const path = require('node:path');
|
|
15
|
-
|
|
16
|
-
const RESEARCH_ABSENT_DIGEST = 'RESEARCH_ABSENT_DIGEST';
|
|
17
|
-
const TASKS_ABSENT_DIGEST = 'TASKS_ABSENT_DIGEST';
|
|
18
|
-
|
|
19
|
-
function asciiOrdinalCompare(a, b) {
|
|
20
|
-
return a < b ? -1 : a > b ? 1 : 0;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// Canonical stable-JSON bytes: object keys sorted by the same ordinal rule
|
|
24
|
-
// (JS's default Array.prototype.sort() on strings already compares by UTF-16
|
|
25
|
-
// code unit, i.e. ASCII ordinal for the ASCII-restricted keys/values in scope
|
|
26
|
-
// here), arrays kept in caller-supplied order. Matches the stableJson
|
|
27
|
-
// convention already used by spec-semantic-model.cjs / validate-spec-output.cjs.
|
|
28
|
-
function stableJson(value) {
|
|
29
|
-
if (Array.isArray(value)) return `[${value.map(stableJson).join(',')}]`;
|
|
30
|
-
if (value && typeof value === 'object') {
|
|
31
|
-
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`).join(',')}}`;
|
|
32
|
-
}
|
|
33
|
-
return JSON.stringify(value);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function sha256Hex(bytes) {
|
|
37
|
-
return crypto.createHash('sha256').update(bytes).digest('hex');
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function sha256Tag(bytes) {
|
|
41
|
-
return `sha256:${sha256Hex(bytes)}`;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Raw-byte sha256 of an exact file's current bytes, no normalization. Throws on
|
|
45
|
-
// a missing/non-regular file so callers cannot silently digest a symlink or a
|
|
46
|
-
// directory as if it were authored content.
|
|
47
|
-
function digestFileBytes(absolutePath) {
|
|
48
|
-
const stat = fs.lstatSync(absolutePath);
|
|
49
|
-
if (!stat.isFile()) throw new Error(`${absolutePath} is not a regular file`);
|
|
50
|
-
return sha256Tag(fs.readFileSync(absolutePath));
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function listTaskFiles(specDir) {
|
|
54
|
-
const tasksDir = path.join(specDir, 'tasks');
|
|
55
|
-
let entries;
|
|
56
|
-
try {
|
|
57
|
-
entries = fs.readdirSync(tasksDir, { withFileTypes: true });
|
|
58
|
-
} catch (error) {
|
|
59
|
-
if (error.code === 'ENOENT') return [];
|
|
60
|
-
throw error;
|
|
61
|
-
}
|
|
62
|
-
return entries
|
|
63
|
-
.filter((entry) => entry.isFile() && entry.name.endsWith('.md'))
|
|
64
|
-
.map((entry) => `tasks/${entry.name}`);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// sha256 over the stable-JSON canonical bytes of an array of {path, digest}
|
|
68
|
-
// objects, one per task file, sorted by path with the exact ASCII ordinal
|
|
69
|
-
// comparator (never localeCompare) — or the exact sentinel when taskless.
|
|
70
|
-
function computeTasksBundleDigest(specDir) {
|
|
71
|
-
const taskPaths = listTaskFiles(specDir);
|
|
72
|
-
if (taskPaths.length === 0) return TASKS_ABSENT_DIGEST;
|
|
73
|
-
const bundle = taskPaths
|
|
74
|
-
.map((relativePath) => ({
|
|
75
|
-
path: relativePath,
|
|
76
|
-
digest: digestFileBytes(path.join(specDir, relativePath)),
|
|
77
|
-
}))
|
|
78
|
-
.sort((a, b) => asciiOrdinalCompare(a.path, b.path));
|
|
79
|
-
return sha256Tag(Buffer.from(stableJson(bundle), 'utf8'));
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Raw-byte sha256 of research.md, or the exact sentinel when absent. Absence is
|
|
83
|
-
// read from the file itself (the durable, unambiguous signal), not from the
|
|
84
|
-
// authoring.research enum, so this never depends on a caller-supplied claim.
|
|
85
|
-
function computeResearchDigest(specDir) {
|
|
86
|
-
const researchPath = path.join(specDir, 'research.md');
|
|
87
|
-
try {
|
|
88
|
-
const stat = fs.lstatSync(researchPath);
|
|
89
|
-
if (!stat.isFile()) return RESEARCH_ABSENT_DIGEST;
|
|
90
|
-
} catch (error) {
|
|
91
|
-
if (error.code === 'ENOENT') return RESEARCH_ABSENT_DIGEST;
|
|
92
|
-
throw error;
|
|
93
|
-
}
|
|
94
|
-
return digestFileBytes(researchPath);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Fresh digests recomputed, at read time, over a spec's exact current authoring
|
|
98
|
-
// bytes — the four C16 stages.
|
|
99
|
-
function computeCurrentDigests(specDir) {
|
|
100
|
-
return {
|
|
101
|
-
requirements: digestFileBytes(path.join(specDir, 'requirements.md')),
|
|
102
|
-
design: digestFileBytes(path.join(specDir, 'design.md')),
|
|
103
|
-
research: computeResearchDigest(specDir),
|
|
104
|
-
tasks: computeTasksBundleDigest(specDir),
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// I20 exact freshness algorithm: a stage is fresh iff its authoring.<field>
|
|
109
|
-
// enum reads "validated" AND the C16 receipt is present with a stored digest
|
|
110
|
-
// for that field that exactly equals a digest freshly recomputed over the
|
|
111
|
-
// field's current bytes. Every other combination reads as draft.
|
|
112
|
-
function isFieldFresh({ authoringValue, receiptEntry, currentDigest }) {
|
|
113
|
-
if (authoringValue !== 'validated') return false;
|
|
114
|
-
if (!receiptEntry || typeof receiptEntry !== 'object') return false;
|
|
115
|
-
if (typeof receiptEntry.digest !== 'string') return false;
|
|
116
|
-
return receiptEntry.digest === currentDigest;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// Convenience wrapper: given a spec.json's own `authoring` and
|
|
120
|
-
// `validation.authoring_validation` (may be absent/null), report freshness for
|
|
121
|
-
// the requested stages against the spec's current on-disk bytes.
|
|
122
|
-
function assertAuthoringFresh({ specDir, authoring, receipt, fields = ['requirements', 'design'] }) {
|
|
123
|
-
const currentDigests = computeCurrentDigests(specDir);
|
|
124
|
-
const report = {};
|
|
125
|
-
for (const field of fields) {
|
|
126
|
-
const authoringValue = authoring && typeof authoring === 'object' ? authoring[field] : undefined;
|
|
127
|
-
const receiptEntry = receipt && typeof receipt === 'object' ? receipt[field] : undefined;
|
|
128
|
-
report[field] = {
|
|
129
|
-
digest: currentDigests[field],
|
|
130
|
-
authoringValue,
|
|
131
|
-
fresh: isFieldFresh({ authoringValue, receiptEntry, currentDigest: currentDigests[field] }),
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
return report;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
module.exports = {
|
|
138
|
-
RESEARCH_ABSENT_DIGEST,
|
|
139
|
-
TASKS_ABSENT_DIGEST,
|
|
140
|
-
asciiOrdinalCompare,
|
|
141
|
-
stableJson,
|
|
142
|
-
sha256Hex,
|
|
143
|
-
sha256Tag,
|
|
144
|
-
digestFileBytes,
|
|
145
|
-
listTaskFiles,
|
|
146
|
-
computeTasksBundleDigest,
|
|
147
|
-
computeResearchDigest,
|
|
148
|
-
computeCurrentDigests,
|
|
149
|
-
isFieldFresh,
|
|
150
|
-
assertAuthoringFresh,
|
|
151
|
-
};
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
// C16/D13 sole atomic writer (I21): the only code path permitted to set an
|
|
5
|
-
// authoring stage to `validated` or to write `validation.authoring_validation`.
|
|
6
|
-
// It builds an in-memory candidate spec.json (never persisted mid-run), proves
|
|
7
|
-
// it clean via the canonical validate-spec-output.cjs + spec-ground.cjs pass
|
|
8
|
-
// over that exact candidate, computes the C16 digests via the shared
|
|
9
|
-
// R0-01-owned spec-authoring-digest.cjs primitive, and only on a fully clean
|
|
10
|
-
// pass atomically replaces spec.json (write-temp-then-rename) with the updated
|
|
11
|
-
// `authoring.*` enum values and the fresh receipt written together. A failed,
|
|
12
|
-
// partial, or exceptional run leaves spec.json completely unchanged.
|
|
13
|
-
|
|
14
|
-
const fs = require('fs');
|
|
15
|
-
const path = require('path');
|
|
16
|
-
const { validateSpec } = require('./validate-spec-output.cjs');
|
|
17
|
-
const { groundSpec } = require('./spec-ground.cjs');
|
|
18
|
-
const DIGEST = require('./spec-authoring-digest.cjs');
|
|
19
|
-
|
|
20
|
-
const AUTHORING_FIELDS = ['requirements', 'design', 'research', 'tasks'];
|
|
21
|
-
|
|
22
|
-
function usage() {
|
|
23
|
-
console.error('Usage: node spec-authoring-validation.cjs specs/<feature> [--root <path>]');
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function resolveSpecDir(input) {
|
|
27
|
-
const cwd = process.cwd();
|
|
28
|
-
const direct = path.resolve(cwd, input);
|
|
29
|
-
if (fs.existsSync(direct)) return direct;
|
|
30
|
-
const viaSpecs = path.resolve(cwd, 'specs', input);
|
|
31
|
-
if (fs.existsSync(viaSpecs)) return viaSpecs;
|
|
32
|
-
return direct;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function isPlainObject(value) {
|
|
36
|
-
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function cloneJson(value) {
|
|
40
|
-
return JSON.parse(JSON.stringify(value));
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// In-memory candidate: every non-absent authoring field flips to `validated`
|
|
44
|
-
// and a fresh C16 receipt (all four stages, always present, non-circular —
|
|
45
|
-
// computed only from the authored artifacts' own current bytes) is attached.
|
|
46
|
-
// Both writes happen together in the same candidate object (I21).
|
|
47
|
-
function buildCandidate(currentSpec, specDir, nowIso) {
|
|
48
|
-
if (!isPlainObject(currentSpec.authoring)) throw new Error('spec.json.authoring must be an object');
|
|
49
|
-
const currentDigests = DIGEST.computeCurrentDigests(specDir);
|
|
50
|
-
const candidate = cloneJson(currentSpec);
|
|
51
|
-
const authoring = { ...candidate.authoring };
|
|
52
|
-
for (const field of AUTHORING_FIELDS) {
|
|
53
|
-
if (authoring[field] !== 'absent') authoring[field] = 'validated';
|
|
54
|
-
}
|
|
55
|
-
candidate.authoring = authoring;
|
|
56
|
-
candidate.updated_at = nowIso;
|
|
57
|
-
candidate.validation = {
|
|
58
|
-
...candidate.validation,
|
|
59
|
-
authoring_validation: {
|
|
60
|
-
schema_version: '1',
|
|
61
|
-
requirements: { digest: currentDigests.requirements, validated_at: nowIso },
|
|
62
|
-
design: { digest: currentDigests.design, validated_at: nowIso },
|
|
63
|
-
research: { digest: currentDigests.research, validated_at: nowIso },
|
|
64
|
-
tasks: { digest: currentDigests.tasks, validated_at: nowIso },
|
|
65
|
-
},
|
|
66
|
-
};
|
|
67
|
-
return candidate;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function atomicReplaceSpec(specJsonPath, candidate) {
|
|
71
|
-
const body = `${JSON.stringify(candidate, null, 2)}\n`;
|
|
72
|
-
const tempPath = `${specJsonPath}.spec-authoring-validation-${process.pid}-${Date.now()}.tmp`;
|
|
73
|
-
fs.writeFileSync(tempPath, body);
|
|
74
|
-
fs.renameSync(tempPath, specJsonPath);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// Runs the coordinator over one spec directory. Never partially persists: a
|
|
78
|
-
// clean validate+ground pass on the candidate is required before any write.
|
|
79
|
-
function runAuthoringValidation({ specDir, root = null }) {
|
|
80
|
-
const specJsonPath = path.join(specDir, 'spec.json');
|
|
81
|
-
const currentSpec = JSON.parse(fs.readFileSync(specJsonPath, 'utf8'));
|
|
82
|
-
const nowIso = new Date().toISOString();
|
|
83
|
-
const candidate = buildCandidate(currentSpec, specDir, nowIso);
|
|
84
|
-
|
|
85
|
-
const validation = validateSpec(specDir, candidate);
|
|
86
|
-
const grounding = groundSpec({ specDir, root, spec: candidate });
|
|
87
|
-
const errors = [
|
|
88
|
-
...validation.errors.map((error) => `validate-spec-output: ${error}`),
|
|
89
|
-
...grounding.errors.map((error) => `spec-ground: ${error}`),
|
|
90
|
-
];
|
|
91
|
-
if (errors.length > 0) return { ok: false, errors, candidate: null };
|
|
92
|
-
|
|
93
|
-
atomicReplaceSpec(specJsonPath, candidate);
|
|
94
|
-
return { ok: true, errors: [], candidate };
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function main() {
|
|
98
|
-
const args = process.argv.slice(2);
|
|
99
|
-
const rootFlagIndex = args.indexOf('--root');
|
|
100
|
-
const root = rootFlagIndex >= 0 ? path.resolve(process.cwd(), args[rootFlagIndex + 1] || '') : null;
|
|
101
|
-
const positional = args.filter((arg, index) => arg !== '--root' && (rootFlagIndex < 0 || index !== rootFlagIndex + 1));
|
|
102
|
-
if (positional.length !== 1) { usage(); process.exit(2); }
|
|
103
|
-
const specDir = resolveSpecDir(positional[0]);
|
|
104
|
-
if (!fs.existsSync(path.join(specDir, 'spec.json'))) {
|
|
105
|
-
console.error(`spec-authoring-validation: no spec.json found at ${specDir}`);
|
|
106
|
-
process.exit(2);
|
|
107
|
-
}
|
|
108
|
-
let result;
|
|
109
|
-
try {
|
|
110
|
-
result = runAuthoringValidation({ specDir, root });
|
|
111
|
-
} catch (error) {
|
|
112
|
-
console.error(`AUTHORING_VALIDATION_ERROR ${specDir}`);
|
|
113
|
-
console.error(`- ${error.message}`);
|
|
114
|
-
process.exit(2);
|
|
115
|
-
}
|
|
116
|
-
if (!result.ok) {
|
|
117
|
-
console.error(`AUTHORING_VALIDATION_FAIL ${specDir}`);
|
|
118
|
-
for (const error of result.errors) console.error(`- ${error}`);
|
|
119
|
-
process.exit(1);
|
|
120
|
-
}
|
|
121
|
-
console.log(`AUTHORING_VALIDATED ${specDir}`);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
module.exports = { runAuthoringValidation, buildCandidate };
|
|
125
|
-
|
|
126
|
-
if (require.main === module) main();
|
|
@@ -1,290 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const crypto = require('crypto');
|
|
4
|
-
const fs = require('fs');
|
|
5
|
-
const path = require('path');
|
|
6
|
-
const SEMANTIC = require('./spec-semantic-model.cjs');
|
|
7
|
-
|
|
8
|
-
const SHA256_RE = /^sha256:[a-f0-9]{64}$/;
|
|
9
|
-
|
|
10
|
-
function plain(value) {
|
|
11
|
-
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function inside(root, target) {
|
|
15
|
-
const relative = path.relative(path.resolve(root), path.resolve(target));
|
|
16
|
-
return relative === '' || (relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function readBoundFile(root, target) {
|
|
20
|
-
const requested = path.isAbsolute(target) ? target : path.resolve(root, target);
|
|
21
|
-
if (!inside(root, requested)) return null;
|
|
22
|
-
try {
|
|
23
|
-
const canonicalRoot = fs.realpathSync(root);
|
|
24
|
-
const canonical = fs.realpathSync(requested);
|
|
25
|
-
if (!inside(canonicalRoot, canonical) || !fs.statSync(canonical).isFile()) return null;
|
|
26
|
-
return fs.readFileSync(canonical);
|
|
27
|
-
} catch { return null; }
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function artifactReferences(body) {
|
|
31
|
-
if (typeof body !== 'string') return [];
|
|
32
|
-
const references = [];
|
|
33
|
-
for (const line of body.split('\n')) {
|
|
34
|
-
if (!/^\s*Artifacts?\s*:/i.test(line) && !/^\s*Artifact\s+produced\b/i.test(line)) continue;
|
|
35
|
-
const hashes = [...line.matchAll(/\b(?:artifact[_-])?sha-?256\s*:\s*([^\s),;`]+)/gi)].map((match) => match[1]);
|
|
36
|
-
const raw = line.replace(/^\s*Artifacts?\s*:\s*/i, '').replace(/^\s*Artifact\s+produced\s*:?[ \t]*/i, '').replace(/\b(?:artifact[_-])?sha-?256\s*:\s*[^\s),;`]+/gi, '').replace(/[()[\]{}]/g, ' ').trim();
|
|
37
|
-
raw.split(/\s*(?:\+|,|;|\band\b)\s*/i).map((value) => value.trim()).filter(Boolean)
|
|
38
|
-
.forEach((artifactPath, index) => references.push({ path: artifactPath, hash: hashes[index] || null }));
|
|
39
|
-
}
|
|
40
|
-
return references;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function validatorApi(validatorPath) {
|
|
44
|
-
let imported;
|
|
45
|
-
try { imported = require(validatorPath); }
|
|
46
|
-
catch (error) { throw new Error(`canonical validator CommonJS API could not be loaded (${error.message})`); }
|
|
47
|
-
if (typeof imported.validateSpec !== 'function' || typeof imported.computeSemanticDigest21 !== 'function') {
|
|
48
|
-
throw new Error('canonical validator must export validateSpec and computeSemanticDigest21 CommonJS APIs');
|
|
49
|
-
}
|
|
50
|
-
return imported;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function stableDigest(value) {
|
|
54
|
-
return `sha256:${crypto.createHash('sha256').update(SEMANTIC.stableJson(value), 'utf8').digest('hex')}`;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function validateTerminalSemanticReview(spec, semanticDigest) {
|
|
58
|
-
const review = spec.validation?.semantic_review;
|
|
59
|
-
if (!plain(review) || review.status !== 'completed') {
|
|
60
|
-
return { ok: false, reason: 'canonical schema 2.1 final state requires a completed semantic review receipt' };
|
|
61
|
-
}
|
|
62
|
-
if (review.verdict !== 'PASS' || review.lifecycle_disposition !== 'CONTINUE') {
|
|
63
|
-
return { ok: false, reason: 'canonical schema 2.1 final state requires semantic review PASS CONTINUE' };
|
|
64
|
-
}
|
|
65
|
-
if (!Array.isArray(review.findings) || !Array.isArray(review.unresolved_decisions)) {
|
|
66
|
-
return { ok: false, reason: 'canonical schema 2.1 final state requires canonical semantic review findings and decisions' };
|
|
67
|
-
}
|
|
68
|
-
const blockingCount = review.findings.filter((finding) => finding?.blocking === true).length
|
|
69
|
-
+ review.unresolved_decisions.filter((decision) => decision?.blocking === true).length;
|
|
70
|
-
if (blockingCount !== 0) {
|
|
71
|
-
return { ok: false, reason: 'canonical schema 2.1 final state requires zero blocking findings or decisions' };
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const entries = spec.validation?.semantic_review_history?.entries;
|
|
75
|
-
if (!Array.isArray(entries) || entries.length === 0) {
|
|
76
|
-
return { ok: false, reason: 'canonical schema 2.1 final state requires a latest semantic review history entry' };
|
|
77
|
-
}
|
|
78
|
-
const latest = entries[entries.length - 1];
|
|
79
|
-
if (latest?.verdict !== 'PASS'
|
|
80
|
-
|| latest?.lifecycle_disposition !== 'CONTINUE'
|
|
81
|
-
|| latest?.blocking_count !== 0) {
|
|
82
|
-
return { ok: false, reason: 'canonical schema 2.1 final state requires latest history PASS CONTINUE with blocking_count 0' };
|
|
83
|
-
}
|
|
84
|
-
if (latest.semantic_digest !== semanticDigest || latest.semantic_digest !== review.semantic_digest) {
|
|
85
|
-
return { ok: false, reason: 'canonical schema 2.1 final state requires latest history and receipt semantic_digest to match' };
|
|
86
|
-
}
|
|
87
|
-
if (!Number.isInteger(review.repair_round) || review.repair_round !== latest.attempt_index) {
|
|
88
|
-
return { ok: false, reason: 'canonical schema 2.1 final state requires receipt repair_round to equal latest history attempt_index' };
|
|
89
|
-
}
|
|
90
|
-
if (latest.review_receipt_digest !== stableDigest(review)) {
|
|
91
|
-
return { ok: false, reason: 'canonical schema 2.1 final state requires latest history review_receipt_digest to match the canonical receipt' };
|
|
92
|
-
}
|
|
93
|
-
return { ok: true };
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function isDurableCloseout(spec) {
|
|
97
|
-
if (!plain(spec)) return false;
|
|
98
|
-
if (spec.schema_version === '2.1') return spec.status === 'done';
|
|
99
|
-
const phase = spec.current_phase || spec.phase;
|
|
100
|
-
return ['done', 'completed', 'complete'].includes(spec.status)
|
|
101
|
-
|| ['closeout', 'completion', 'completed', 'complete'].includes(phase);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function isValidLifecycleStatus(spec) {
|
|
105
|
-
if (!plain(spec)) return false;
|
|
106
|
-
if (spec.schema_version === '2.1') return ['in_progress', 'paused', 'blocked', 'done'].includes(spec.status);
|
|
107
|
-
return ['in_progress', 'in-progress', 'paused', 'blocked', 'done', 'completed', 'complete'].includes(spec.status);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function validateCanonicalFinalState({ policy, projectRoot, candidate, dependencies }) {
|
|
111
|
-
if (candidate.spec.schema_version !== '2.1') return { ok: true, legacy: true, semanticDigest: null };
|
|
112
|
-
const featureDir = candidate.featureDir || path.dirname(candidate.specFile);
|
|
113
|
-
const review = candidate.spec.validation?.semantic_review;
|
|
114
|
-
if (candidate.spec.ready_for_implementation !== true
|
|
115
|
-
|| candidate.spec.validation?.status !== 'completed'
|
|
116
|
-
|| review?.status !== 'completed'
|
|
117
|
-
|| !SHA256_RE.test(review?.semantic_digest || '')) {
|
|
118
|
-
return { ok: false, reason: 'canonical schema 2.1 terminal state requires completed readiness and a bound semantic digest' };
|
|
119
|
-
}
|
|
120
|
-
let apis;
|
|
121
|
-
try { apis = dependencies(); }
|
|
122
|
-
catch (error) { return { ok: false, reason: error.message }; }
|
|
123
|
-
let validation;
|
|
124
|
-
try { validation = apis.validator.validateSpec(featureDir); }
|
|
125
|
-
catch (error) { return { ok: false, reason: `canonical schema 2.1 validation failed safely (${error.message})` }; }
|
|
126
|
-
if (!validation || !Array.isArray(validation.errors) || validation.errors.length > 0) {
|
|
127
|
-
return { ok: false, reason: `canonical schema 2.1 validation failed (${(validation?.errors || ['invalid validator result']).slice(0, 4).join('; ')})` };
|
|
128
|
-
}
|
|
129
|
-
let grounding;
|
|
130
|
-
try { grounding = apis.grounder.groundSpec({ specDir: featureDir, root: projectRoot, spec: candidate.spec }); }
|
|
131
|
-
catch (error) { return { ok: false, reason: `canonical schema 2.1 grounding failed safely (${error.message})` }; }
|
|
132
|
-
if (!grounding || !Array.isArray(grounding.errors) || grounding.errors.length > 0) {
|
|
133
|
-
return { ok: false, reason: `canonical schema 2.1 grounding failed (${(grounding?.errors || ['invalid grounder result']).slice(0, 4).join('; ')})` };
|
|
134
|
-
}
|
|
135
|
-
let digest;
|
|
136
|
-
try {
|
|
137
|
-
const result = apis.validator.computeSemanticDigest21(featureDir, candidate.spec);
|
|
138
|
-
if (!result || !Array.isArray(result.errors) || result.errors.length > 0 || !SHA256_RE.test(result.digest || '')) {
|
|
139
|
-
return { ok: false, reason: `canonical semantic digest could not be recomputed (${(result?.errors || ['invalid digest result']).slice(0, 4).join('; ')})` };
|
|
140
|
-
}
|
|
141
|
-
digest = result.digest;
|
|
142
|
-
} catch (error) {
|
|
143
|
-
return { ok: false, reason: `canonical semantic digest could not be recomputed (${error.message})` };
|
|
144
|
-
}
|
|
145
|
-
if (digest !== review.semantic_digest) return { ok: false, reason: 'canonical semantic digest is stale for the current final-state artifacts' };
|
|
146
|
-
const terminalReview = validateTerminalSemanticReview(candidate.spec, digest);
|
|
147
|
-
if (!terminalReview.ok) return { ok: false, reason: terminalReview.reason };
|
|
148
|
-
if (policy.readWorkflowPolicySnapshot(candidate.spec).assurance_level === 'Strict') {
|
|
149
|
-
const observed = apis.semanticAuthority.verifyAttestation(projectRoot, candidate.specFile, candidate.featureName, digest);
|
|
150
|
-
if (!observed?.ok) return { ok: false, reason: `Strict semantic authority requires an allowlisted host-hook-observed reviewer PASS event (${observed?.reason || 'observation unavailable'})` };
|
|
151
|
-
}
|
|
152
|
-
return { ok: true, legacy: false, semanticDigest: digest };
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function configuredProjectMinimum(policy, runtime = {}) {
|
|
156
|
-
const configured = runtime.spec?.policy_minimum;
|
|
157
|
-
if (configured === undefined) return policy.canonicalWorkflowPolicySnapshot({ planning_depth: 'Compact', assurance_level: 'Routine' });
|
|
158
|
-
if (!plain(configured)) throw new Error('runtime.spec.policy_minimum must be a canonical policy object');
|
|
159
|
-
return policy.canonicalWorkflowPolicySnapshot(Object.prototype.hasOwnProperty.call(configured, 'version') ? { workflow_policy: configured } : configured);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function axisRank(values, value) { return Array.isArray(values) ? values.indexOf(value) : -1; }
|
|
163
|
-
function canonicalPolicy(policy, snapshot) { return policy.canonicalWorkflowPolicySnapshot({ workflow_policy: snapshot }); }
|
|
164
|
-
function policyView(policy, snapshot) { return policy.readWorkflowPolicySnapshot({ workflow_policy: canonicalPolicy(policy, snapshot) }); }
|
|
165
|
-
|
|
166
|
-
function policyMonotonicFailures(policy, baseline, current) {
|
|
167
|
-
const failures = [];
|
|
168
|
-
const before = policyView(policy, baseline);
|
|
169
|
-
const after = policyView(policy, current);
|
|
170
|
-
for (const [field, values] of [['planning_depth', policy.PLANNING_DEPTHS], ['automatic_planning_depth', policy.PLANNING_DEPTHS], ['assurance_level', policy.ASSURANCE_LEVELS], ['automatic_assurance_level', policy.ASSURANCE_LEVELS]]) {
|
|
171
|
-
if (axisRank(values, after[field]) < axisRank(values, before[field])) failures.push(`workflow_policy.${field === 'automatic_planning_depth' ? 'classified_minimum.planning_depth' : field === 'automatic_assurance_level' ? 'classified_minimum.assurance_level' : field} downgraded from ${before[field]} to ${after[field]}`);
|
|
172
|
-
}
|
|
173
|
-
const risks = new Set(after.risks);
|
|
174
|
-
for (const risk of before.risks) if (!risks.has(risk)) failures.push(`workflow_policy risk ${risk} was removed`);
|
|
175
|
-
const obligations = new Set(after.proof_obligations);
|
|
176
|
-
for (const obligation of before.proof_obligations) if (!obligations.has(obligation)) failures.push(`workflow_policy obligation ${obligation} was removed`);
|
|
177
|
-
return failures;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function mergePolicyFloor(policy, floor, current) {
|
|
181
|
-
const merged = { ...current, risks: [...new Set([...(current.risks || []), ...(floor.risks || [])])] };
|
|
182
|
-
for (const [field, values] of [['planning_depth', policy.PLANNING_DEPTHS], ['automatic_planning_depth', policy.PLANNING_DEPTHS], ['assurance_level', policy.ASSURANCE_LEVELS], ['automatic_assurance_level', policy.ASSURANCE_LEVELS]]) {
|
|
183
|
-
if (axisRank(values, floor[field]) > axisRank(values, current[field])) merged[field] = floor[field];
|
|
184
|
-
}
|
|
185
|
-
const riskFloor = policy.classifyLane({ risks: merged.risks, taskCount: 1 }).automaticAssuranceLevel;
|
|
186
|
-
if (axisRank(policy.ASSURANCE_LEVELS, riskFloor) > axisRank(policy.ASSURANCE_LEVELS, merged.automatic_assurance_level)) merged.automatic_assurance_level = riskFloor;
|
|
187
|
-
if (axisRank(policy.ASSURANCE_LEVELS, merged.automatic_assurance_level) > axisRank(policy.ASSURANCE_LEVELS, merged.assurance_level)) merged.assurance_level = merged.automatic_assurance_level;
|
|
188
|
-
merged.lane = policy.compatibilityLane(merged.planning_depth, merged.assurance_level);
|
|
189
|
-
merged.automatic_lane = policy.compatibilityLane(merged.automatic_planning_depth, merged.automatic_assurance_level);
|
|
190
|
-
merged.artifact_profile = { None: 'targeted', Compact: 'bounded', Full: 'strict' }[merged.planning_depth];
|
|
191
|
-
merged.planning_obligations = policy.planningObligationsFor(merged.planning_depth);
|
|
192
|
-
merged.proof_obligations = policy.obligationsForAssurance(merged.assurance_level, merged.risks);
|
|
193
|
-
merged.actor_needs = policy.actorNeedsFor(merged.proof_obligations);
|
|
194
|
-
return merged;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
function observePolicyBaseline({ state, policy, projectRoot, candidate, runtime }) {
|
|
198
|
-
if (!Object.prototype.hasOwnProperty.call(candidate.spec, 'workflow_policy')) return { ok: false, reason: 'workflow_policy is missing for persisted spec' };
|
|
199
|
-
const validation = policy.validateWorkflowPolicySnapshot(candidate.spec.workflow_policy);
|
|
200
|
-
if (!validation.valid) return { ok: false, reason: `workflow_policy is malformed: ${validation.errors.join('; ')}` };
|
|
201
|
-
const current = canonicalPolicy(policy, candidate.spec.workflow_policy);
|
|
202
|
-
try {
|
|
203
|
-
const identity = { project_root: fs.realpathSync(projectRoot), spec_file: fs.realpathSync(candidate.specFile), feature_name: candidate.featureName };
|
|
204
|
-
state.ensureKey();
|
|
205
|
-
let authorityState = state.readState(projectRoot);
|
|
206
|
-
if (authorityState.malformed || authorityState.keyError || authorityState.keyMissing) return { ok: false, reason: authorityState.reason || 'completion authority state is unavailable' };
|
|
207
|
-
const projectMinimum = configuredProjectMinimum(policy, runtime);
|
|
208
|
-
const minimumFailures = policyMonotonicFailures(policy, projectMinimum, current);
|
|
209
|
-
if (minimumFailures.length) return { ok: false, reason: `workflow_policy is below the configured project minimum (${minimumFailures.join('; ')})` };
|
|
210
|
-
if (!authorityState.floor || state.digest(authorityState.floor.policy) !== state.digest(projectMinimum)) {
|
|
211
|
-
state.writePolicyFloor(projectRoot, projectMinimum);
|
|
212
|
-
authorityState = state.readState(projectRoot);
|
|
213
|
-
if (authorityState.malformed || authorityState.keyError || authorityState.keyMissing || !authorityState.floor) return { ok: false, reason: authorityState.reason || 'configured project policy minimum could not be persisted' };
|
|
214
|
-
}
|
|
215
|
-
const baseline = state.readBaseline(projectRoot, identity);
|
|
216
|
-
if (!baseline) { state.writeBaseline(projectRoot, identity, current); return { ok: true }; }
|
|
217
|
-
const baselineValidation = policy.validateWorkflowPolicySnapshot(baseline.policy);
|
|
218
|
-
if (!baselineValidation.valid) return { ok: false, reason: `persisted workflow policy baseline is malformed: ${baselineValidation.errors.join('; ')}` };
|
|
219
|
-
const failures = policyMonotonicFailures(policy, baseline.policy, current);
|
|
220
|
-
if (failures.length) return { ok: false, reason: `workflow_policy baseline is monotonic and cannot be downgraded (${failures.join('; ')})` };
|
|
221
|
-
if (state.digest(baseline.policy) !== state.digest(current)) state.writeBaseline(projectRoot, identity, current);
|
|
222
|
-
return { ok: true };
|
|
223
|
-
} catch (error) { return { ok: false, reason: `workflow_policy baseline unavailable: ${error.message}` }; }
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
function createAuthority(adapter) {
|
|
227
|
-
if (!adapter || typeof adapter.resolveCandidate !== 'function' || !adapter.state || !adapter.receipts || typeof adapter.dependencies !== 'function') {
|
|
228
|
-
throw new Error('final-state runtime adapter is incomplete');
|
|
229
|
-
}
|
|
230
|
-
const finalStateDependencies = (overrides = {}) => {
|
|
231
|
-
if (overrides.validator && overrides.grounder && overrides.semanticAuthority) return overrides;
|
|
232
|
-
const loaded = adapter.dependencies(overrides);
|
|
233
|
-
if (typeof loaded.validator?.validateSpec !== 'function' || typeof loaded.validator?.computeSemanticDigest21 !== 'function' || typeof loaded.grounder?.groundSpec !== 'function' || typeof loaded.semanticAuthority?.verifyAttestation !== 'function') throw new Error('canonical final-state validator, grounder, or semantic authority API is unavailable');
|
|
234
|
-
return loaded;
|
|
235
|
-
};
|
|
236
|
-
const resolveCandidate = (input) => adapter.resolveCandidate(input);
|
|
237
|
-
const validate = (input) => validateCanonicalFinalState({ ...input, dependencies: () => finalStateDependencies(input.dependencies || {}) });
|
|
238
|
-
|
|
239
|
-
function evaluateCloseout({ policy, projectRoot, runtime = {}, payload = {}, finalState = {}, resolver }) {
|
|
240
|
-
if (payload.stop_hook_active === true) return { ok: true, active: false, candidate: null };
|
|
241
|
-
const candidate = resolveCandidate({ resolver, projectRoot, runtime, payload });
|
|
242
|
-
if (!candidate) return { ok: true, candidate: null };
|
|
243
|
-
if (candidate.error) return { ok: false, candidate, reason: `${candidate.error}: ${Array.isArray(candidate.candidates) ? candidate.candidates.join(', ') : (candidate.reason || candidate.error)}` };
|
|
244
|
-
if (!isValidLifecycleStatus(candidate.spec)) return { ok: false, candidate, reason: `spec status is invalid: ${String(candidate.spec.status)}` };
|
|
245
|
-
if (!isDurableCloseout(candidate.spec)) return { ok: true, active: false, candidate };
|
|
246
|
-
const canonical = validate({ policy, projectRoot, candidate, dependencies: finalState });
|
|
247
|
-
if (!canonical.ok) return { ok: false, candidate, reason: canonical.reason };
|
|
248
|
-
const sessionId = payload.session_id;
|
|
249
|
-
if (typeof sessionId !== 'string' || !sessionId.trim()) return { ok: false, candidate, reason: 'trusted host session ID is unavailable' };
|
|
250
|
-
const baseline = observePolicyBaseline({ state: adapter.state, policy, projectRoot, candidate, runtime });
|
|
251
|
-
if (!baseline.ok) return { ok: false, candidate, reason: baseline.reason };
|
|
252
|
-
if (runtime.spec?.completion_gate !== undefined && runtime.spec.completion_gate !== true) return { ok: false, candidate, reason: 'runtime.spec.completion_gate is worker-writable and cannot authorize completion' };
|
|
253
|
-
const registry = plain(candidate.spec.task_registry) ? candidate.spec.task_registry : {};
|
|
254
|
-
const featureDir = path.join(candidate.specsDir, candidate.featureName);
|
|
255
|
-
if (Object.keys(registry).length === 0 && !policy.isCanonicalTasklessTerminalSpec(candidate.spec, featureDir)) return { ok: false, candidate, reason: 'task_registry is missing or empty outside canonical schema 2.1 taskless topology' };
|
|
256
|
-
let runtimeContext;
|
|
257
|
-
try { runtimeContext = policy.deriveRuntimeContext({ projectRoot, specsRoot: candidate.specsDir, specFile: candidate.specFile, featureName: candidate.featureName, runtimeSession: sessionId }); }
|
|
258
|
-
catch (error) { return { ok: false, candidate, reason: `runtime provenance unavailable: ${error.message}` }; }
|
|
259
|
-
if (!policy.isTrustedRuntimeContext(runtimeContext)) return { ok: false, candidate, reason: 'fresh trusted runtime Base/Head/context is unavailable' };
|
|
260
|
-
const receiptBodies = [];
|
|
261
|
-
const taskFiles = [];
|
|
262
|
-
const failures = [];
|
|
263
|
-
for (const [taskPath, task] of Object.entries(registry)) {
|
|
264
|
-
if (!plain(task) || task.status !== 'done') { failures.push(`${taskPath}: status is not done`); continue; }
|
|
265
|
-
if (policy.isStaleFlashDone(task)) failures.push(`${taskPath}: FLASH_UNVERIFIED`);
|
|
266
|
-
const receipt = adapter.receipts.task(featureDir, taskPath, task, runtimeContext, policy);
|
|
267
|
-
if (receipt.failures.length) failures.push(`${taskPath}: checks ${receipt.failures.join(',')}`);
|
|
268
|
-
if (receipt.body) receiptBodies.push(receipt.body);
|
|
269
|
-
if (receipt.taskFile && receipt.receiptBytes) taskFiles.push({ path: taskPath, task_digest: crypto.createHash('sha256').update(receipt.taskFile.bytes).digest('hex'), receipt_digest: crypto.createHash('sha256').update(receipt.receiptBytes).digest('hex'), receipt_source: receipt.source });
|
|
270
|
-
}
|
|
271
|
-
const featureReceipt = adapter.receipts.feature(featureDir, runtimeContext, policy);
|
|
272
|
-
if (featureReceipt.failures.length) failures.push(`feature-receipt.md: checks ${featureReceipt.failures.join(',')}`);
|
|
273
|
-
if (failures.length) return { ok: false, candidate, reason: `technical completion proof is incomplete (${failures.slice(0, 4).join('; ')})` };
|
|
274
|
-
let completion;
|
|
275
|
-
try { completion = policy.completionDecisionForSpec(candidate.spec, { runtimeContext, executionReceipt: featureReceipt.body, taskContext: {} }); }
|
|
276
|
-
catch (error) { return { ok: false, candidate, reason: `workflow completion proof could not be evaluated: ${error.message}` }; }
|
|
277
|
-
if (!['complete', 'not_applicable'].includes(completion.completion)) return { ok: false, candidate, reason: completion.blocker || 'workflow completion proof is incomplete' };
|
|
278
|
-
if (featureReceipt.body) receiptBodies.push(featureReceipt.body);
|
|
279
|
-
const artifactFiles = receiptBodies.flatMap(artifactReferences).sort((a, b) => `${a.path}:${a.hash}`.localeCompare(`${b.path}:${b.hash}`)).map((reference) => ({ ...reference, digest: crypto.createHash('sha256').update(readBoundFile(projectRoot, reference.path) || Buffer.from('<missing>')).digest('hex') }));
|
|
280
|
-
const digest = adapter.state.digest;
|
|
281
|
-
const specBytes = readBoundFile(projectRoot, candidate.specFile);
|
|
282
|
-
const binding = { project_root: runtimeContext.project_root, platform: process.platform, session_id: sessionId, feature_name: candidate.featureName, spec_file: candidate.specFile, runtime: { base: runtimeContext.base, head: runtimeContext.head, context_id: runtimeContext.context_id }, spec_digest: digest(candidate.spec), spec_bytes_digest: crypto.createHash('sha256').update(specBytes || Buffer.from('<missing>')).digest('hex'), policy_digest: digest(candidate.spec.workflow_policy ?? null), task_registry_digest: digest(registry), task_digest: digest(taskFiles), semantic_digest: canonical.semanticDigest, feature_receipt_digest: crypto.createHash('sha256').update(featureReceipt.receiptBytes).digest('hex'), artifact_digest: digest(artifactFiles), provenance_digest: digest({ runtime: runtimeContext, receipts: receiptBodies.map((body) => body.match(/^\s*(?:Base|Head|base_sha|head_sha)\s*:.*$/gim) || []) }) };
|
|
283
|
-
binding.binding_digest = digest(binding);
|
|
284
|
-
return { ok: true, active: true, candidate, runtimeContext, binding };
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
return { configuredProjectMinimum, evaluateCloseout, finalStateDependencies, isDurableCloseout, isValidLifecycleStatus, mergePolicyFloor, observePolicyBaseline: (input) => observePolicyBaseline({ ...input, state: input.STATE || adapter.state }), policyMonotonicFailures, resolveCandidate, validateCanonicalFinalState: validate, validatorApi };
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
module.exports = { createAuthority, isDurableCloseout, isValidLifecycleStatus, validateCanonicalFinalState, validatorApi };
|