@haposoft/cafekit 0.16.0-rc.4 → 0.16.0-rc.5
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/bin/__tests__/change-firewall.semantic-firewall.test.js +2134 -0
- package/bin/__tests__/codex-hooks.test.js +136 -64
- package/bin/__tests__/develop-contract.test.js +55 -17
- package/bin/__tests__/finalizer-pass-gate.semantic-firewall.test.js +342 -0
- package/bin/__tests__/package-inventory.test.js +29 -11
- package/bin/__tests__/release-preflight.semantic-firewall.test.js +578 -0
- package/bin/__tests__/review-receipt-c2.semantic-firewall.test.js +378 -0
- package/bin/__tests__/specs-v2-execution-closeout.test.js +59 -12
- package/bin/__tests__/specs-v2-policy-and-scaffold.test.js +78 -14
- package/bin/__tests__/specs-v2-validator-grounder-contract.test.js +222 -42
- package/bin/__tests__/validator-grounding.test.js +103 -4
- package/package.json +5 -2
- package/src/claude/hooks/__tests__/completion-authority.test.js +31 -1
- package/src/claude/hooks/__tests__/privacy-block.test.js +111 -29
- package/src/claude/hooks/privacy-block.cjs +38 -14
- package/src/claude/scripts/change-firewall.cjs +1748 -0
- package/src/claude/scripts/spec-authoring-digest.cjs +151 -0
- package/src/claude/scripts/spec-authoring-validation.cjs +126 -0
- package/src/claude/scripts/spec-final-state.cjs +46 -0
- package/src/claude/scripts/spec-ground.cjs +46 -10
- package/src/claude/scripts/spec-readiness.cjs +324 -17
- package/src/claude/scripts/spec-scaffold.cjs +268 -28
- package/src/claude/scripts/validate-spec-output.cjs +262 -10
- package/src/claude/skills/specs/references/review.md +14 -2
- package/src/claude/skills/specs/templates/spec-state.json +14 -2
- package/src/codex/hooks/lib/privacy-state.cjs +23 -22
- package/src/codex/hooks/privacy-approval.cjs +7 -3
- package/src/codex/hooks/privacy-block.cjs +45 -22
|
@@ -28,6 +28,7 @@ const path = require('path');
|
|
|
28
28
|
const crypto = require('crypto');
|
|
29
29
|
const POLICY = require('./workflow-policy.cjs');
|
|
30
30
|
const SEMANTIC = require('./spec-semantic-model.cjs');
|
|
31
|
+
const AUTHORING_DIGEST = require('./spec-authoring-digest.cjs');
|
|
31
32
|
const PATH_SAFETY = require('../hooks/lib/runtime-path-safety.cjs');
|
|
32
33
|
|
|
33
34
|
const TEMPLATES = path.join(__dirname, '..', 'skills', 'specs', 'templates');
|
|
@@ -81,9 +82,29 @@ const CANONICAL_SPEC_FIELDS = new Set([
|
|
|
81
82
|
]);
|
|
82
83
|
const SCOPE_LOCK_FIELDS = ['source', 'in_scope', 'out_of_scope', 'expansion_policy'];
|
|
83
84
|
const COORDINATION_FIELDS = new Set(['boundaries', 'phases']);
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const
|
|
85
|
+
// Schema 2.0 legacy shapes (pre-Specs-v2.1 migration input) are frozen as-is;
|
|
86
|
+
// they never gain the C2/C13/C16 fields below, which are canonical-2.1-only.
|
|
87
|
+
const LEGACY_VALIDATION_FIELDS = ['status', 'semantic_review'];
|
|
88
|
+
const LEGACY_SEMANTIC_REVIEW_FIELDS = ['status', 'semantic_digest', 'reviewed_criteria', 'counterexamples', 'reviewed_artifact_digest'];
|
|
89
|
+
// Canonical schema 2.1 (R1-01, frozen C2/C13/C16).
|
|
90
|
+
const VALIDATION_FIELDS = ['status', 'semantic_review', 'semantic_review_history', 'authoring_validation'];
|
|
91
|
+
const PRE_C2_SEMANTIC_REVIEW_FIELDS = ['status', 'semantic_digest', 'reviewed_criteria', 'counterexamples'];
|
|
92
|
+
const SEMANTIC_REVIEW_FIELDS = [
|
|
93
|
+
'status', 'semantic_digest', 'verdict', 'lifecycle_disposition', 'findings',
|
|
94
|
+
'unresolved_decisions', 'graph_coverage', 'repair_round', 'reviewed_criteria',
|
|
95
|
+
'counterexamples', 'reviewer_evidence',
|
|
96
|
+
];
|
|
97
|
+
const SEMANTIC_REVIEW_FINDING_FIELDS = ['id', 'severity', 'location', 'summary', 'blocking', 'disposition'];
|
|
98
|
+
const SEMANTIC_REVIEW_DECISION_FIELDS = ['id', 'summary', 'blocking'];
|
|
99
|
+
const SEMANTIC_REVIEW_COVERAGE_FIELDS = ['surface', 'covered', 'notes'];
|
|
100
|
+
const SEMANTIC_REVIEW_EVIDENCE_FIELDS = ['assurance', 'summary'];
|
|
101
|
+
const SEMANTIC_REVIEW_HISTORY_FIELDS = ['lineage_id', 'entries'];
|
|
102
|
+
const SEMANTIC_REVIEW_HISTORY_ENTRY_FIELDS = [
|
|
103
|
+
'sequence', 'semantic_digest', 'review_receipt_digest', 'verdict',
|
|
104
|
+
'lifecycle_disposition', 'blocking_count', 'attempt_index', 'review_epoch',
|
|
105
|
+
];
|
|
106
|
+
const AUTHORING_VALIDATION_FIELDS = ['schema_version', 'requirements', 'design', 'research', 'tasks'];
|
|
107
|
+
const AUTHORING_VALIDATION_STAGE_FIELDS = ['digest', 'validated_at'];
|
|
87
108
|
const LEGACY_SPEC_FIELDS = [
|
|
88
109
|
...CANONICAL_SPEC_FIELDS,
|
|
89
110
|
'approvals', 'current_phase', 'override_receipt', 'timestamps',
|
|
@@ -108,8 +129,13 @@ const TRANSACTION_FORMAT = 'cafekit-spec-scaffold-transaction/1';
|
|
|
108
129
|
|
|
109
130
|
class ScaffoldPreconditionError extends Error {}
|
|
110
131
|
|
|
132
|
+
const USAGE_TEXT = 'Usage: node spec-scaffold.cjs <feature> [--sync-semantic-model] [--tasks "R0-01-slug,R1-01-slug" --boundaries \'[{"id":"B-OWN","type":"ownership",...}]\'] [--lane Standard|Critical] [--planning-depth Compact|Full] [--assurance-level Routine|Elevated|Strict] [--research --uncertainty "..."] [--phases \'[{"id":"phase-1","task_ids":["R1-01"],"entry_condition":"...","exit_condition":"...","owner_boundary":"B-OWN"}]\'] [--risks auth,privacy] [--lang en] [--title "..."] [--specs-root specs]';
|
|
133
|
+
|
|
134
|
+
// Usage reaches stderr when it accompanies a precondition failure, and stdout
|
|
135
|
+
// when the caller asked for it with --help; a requested help text is output,
|
|
136
|
+
// not a diagnostic.
|
|
111
137
|
function usage() {
|
|
112
|
-
console.error(
|
|
138
|
+
console.error(USAGE_TEXT);
|
|
113
139
|
}
|
|
114
140
|
|
|
115
141
|
function failPrecondition(message) {
|
|
@@ -278,15 +304,35 @@ function pendingApproval() {
|
|
|
278
304
|
return 'draft';
|
|
279
305
|
}
|
|
280
306
|
|
|
307
|
+
// C2 SemanticReviewReceipt (R1-01, frozen): not-run keeps every receipt field
|
|
308
|
+
// null/empty, including the separate verdict and lifecycle_disposition and
|
|
309
|
+
// the finalizer-derived repair_round attempt index.
|
|
281
310
|
function semanticReviewNotRun() {
|
|
282
311
|
return {
|
|
283
312
|
status: 'not-run',
|
|
284
313
|
semantic_digest: null,
|
|
314
|
+
verdict: null,
|
|
315
|
+
lifecycle_disposition: null,
|
|
316
|
+
findings: [],
|
|
317
|
+
unresolved_decisions: [],
|
|
318
|
+
graph_coverage: [],
|
|
319
|
+
repair_round: 0,
|
|
285
320
|
reviewed_criteria: [],
|
|
286
321
|
counterexamples: [],
|
|
322
|
+
reviewer_evidence: null,
|
|
287
323
|
};
|
|
288
324
|
}
|
|
289
325
|
|
|
326
|
+
// C13 SemanticReviewHistory (R1-01, frozen): lineage_id is derived once, from
|
|
327
|
+
// the immutable feature_name/created_at pair, and never set by a caller.
|
|
328
|
+
function computeLineageId(featureName, createdAt) {
|
|
329
|
+
return `sha256:${crypto.createHash('sha256').update(SEMANTIC.stableJson({ feature_name: featureName, created_at: createdAt })).digest('hex')}`;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function semanticReviewHistoryInitial(featureName, createdAt) {
|
|
333
|
+
return { lineage_id: computeLineageId(featureName, createdAt), entries: [] };
|
|
334
|
+
}
|
|
335
|
+
|
|
290
336
|
function parseArtifactDeclarations(raw, parsedTasks) {
|
|
291
337
|
if (raw === null || raw === undefined) return new Map();
|
|
292
338
|
let declarations;
|
|
@@ -716,7 +762,7 @@ function canonicalRegistryFields(entry) {
|
|
|
716
762
|
return hasOwn(entry, 'artifacts') ? [...REGISTRY_KEYS, 'artifacts'] : REGISTRY_KEYS;
|
|
717
763
|
}
|
|
718
764
|
|
|
719
|
-
function assertCanonicalSpecInputClosedWorld(spec) {
|
|
765
|
+
function assertCanonicalSpecInputClosedWorld(spec, { allowLegacyReview = false } = {}) {
|
|
720
766
|
if (!isPlainObject(spec) || spec.schema_version !== '2.1') return;
|
|
721
767
|
const topLevel = [...CANONICAL_SPEC_FIELDS].filter((field) => {
|
|
722
768
|
if (field === 'research') return hasOwn(spec, 'research');
|
|
@@ -730,10 +776,29 @@ function assertCanonicalSpecInputClosedWorld(spec) {
|
|
|
730
776
|
const coordinationFields = hasOwn(spec.coordination, 'phases')
|
|
731
777
|
? [...COORDINATION_FIELDS] : ['boundaries'];
|
|
732
778
|
assertExactKeys(spec.coordination, coordinationFields, 'canonical schema 2.1 coordination');
|
|
733
|
-
|
|
779
|
+
const reviewKeys = isPlainObject(spec.validation?.semantic_review)
|
|
780
|
+
? Object.keys(spec.validation.semantic_review).sort().join(',') : '';
|
|
781
|
+
const legacyReviewFields = reviewKeys === [...LEGACY_SEMANTIC_REVIEW_FIELDS].sort().join(',')
|
|
782
|
+
? LEGACY_SEMANTIC_REVIEW_FIELDS
|
|
783
|
+
: reviewKeys === [...PRE_C2_SEMANTIC_REVIEW_FIELDS].sort().join(',')
|
|
784
|
+
? PRE_C2_SEMANTIC_REVIEW_FIELDS : null;
|
|
785
|
+
const legacyReview = allowLegacyReview
|
|
786
|
+
&& isPlainObject(spec.validation)
|
|
787
|
+
&& Object.keys(spec.validation).sort().join(',') === ['semantic_review', 'status'].sort().join(',')
|
|
788
|
+
&& legacyReviewFields !== null;
|
|
789
|
+
// semantic_review_history and authoring_validation (C13/C16) are required
|
|
790
|
+
// going forward, but a pre-existing 2.1 spec authored before this feature's
|
|
791
|
+
// cutover may not carry them yet; accept their absence here so scaffold can
|
|
792
|
+
// migrate such a spec forward (normalizeSpec21Candidate forces a fresh reset
|
|
793
|
+
// whenever either is missing) instead of hard-failing on an old file.
|
|
794
|
+
const validationFields = legacyReview ? ['status', 'semantic_review'] : [...VALIDATION_FIELDS].filter((field) => {
|
|
795
|
+
if (field === 'semantic_review_history' || field === 'authoring_validation') return hasOwn(spec.validation, field);
|
|
796
|
+
return true;
|
|
797
|
+
});
|
|
798
|
+
assertExactKeys(spec.validation, validationFields, 'canonical schema 2.1 validation');
|
|
734
799
|
assertExactKeys(
|
|
735
800
|
spec.validation.semantic_review,
|
|
736
|
-
SEMANTIC_REVIEW_FIELDS,
|
|
801
|
+
legacyReview ? legacyReviewFields : SEMANTIC_REVIEW_FIELDS,
|
|
737
802
|
'canonical schema 2.1 validation.semantic_review',
|
|
738
803
|
);
|
|
739
804
|
if (Array.isArray(spec.validation.semantic_review?.counterexamples)) {
|
|
@@ -746,6 +811,38 @@ function assertCanonicalSpecInputClosedWorld(spec) {
|
|
|
746
811
|
);
|
|
747
812
|
}
|
|
748
813
|
}
|
|
814
|
+
for (const [index, finding] of (spec.validation.semantic_review?.findings || []).entries()) {
|
|
815
|
+
assertExactKeys(finding, SEMANTIC_REVIEW_FINDING_FIELDS, `canonical schema 2.1 validation.semantic_review.findings[${index}]`);
|
|
816
|
+
}
|
|
817
|
+
for (const [index, decision] of (spec.validation.semantic_review?.unresolved_decisions || []).entries()) {
|
|
818
|
+
assertExactKeys(decision, SEMANTIC_REVIEW_DECISION_FIELDS, `canonical schema 2.1 validation.semantic_review.unresolved_decisions[${index}]`);
|
|
819
|
+
}
|
|
820
|
+
for (const [index, coverage] of (spec.validation.semantic_review?.graph_coverage || []).entries()) {
|
|
821
|
+
assertExactKeys(coverage, SEMANTIC_REVIEW_COVERAGE_FIELDS, `canonical schema 2.1 validation.semantic_review.graph_coverage[${index}]`);
|
|
822
|
+
}
|
|
823
|
+
if (!legacyReview && spec.validation.semantic_review?.reviewer_evidence !== null) {
|
|
824
|
+
assertExactKeys(
|
|
825
|
+
spec.validation.semantic_review?.reviewer_evidence,
|
|
826
|
+
SEMANTIC_REVIEW_EVIDENCE_FIELDS,
|
|
827
|
+
'canonical schema 2.1 validation.semantic_review.reviewer_evidence',
|
|
828
|
+
);
|
|
829
|
+
}
|
|
830
|
+
if (hasOwn(spec.validation, 'semantic_review_history')) {
|
|
831
|
+
assertExactKeys(spec.validation.semantic_review_history, SEMANTIC_REVIEW_HISTORY_FIELDS, 'canonical schema 2.1 validation.semantic_review_history');
|
|
832
|
+
for (const [index, entry] of (spec.validation.semantic_review_history?.entries || []).entries()) {
|
|
833
|
+
assertExactKeys(entry, SEMANTIC_REVIEW_HISTORY_ENTRY_FIELDS, `canonical schema 2.1 validation.semantic_review_history.entries[${index}]`);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
if (hasOwn(spec.validation, 'authoring_validation') && spec.validation.authoring_validation !== null) {
|
|
837
|
+
assertExactKeys(spec.validation.authoring_validation, AUTHORING_VALIDATION_FIELDS, 'canonical schema 2.1 validation.authoring_validation');
|
|
838
|
+
for (const stage of ['requirements', 'design', 'research', 'tasks']) {
|
|
839
|
+
assertExactKeys(
|
|
840
|
+
spec.validation.authoring_validation?.[stage],
|
|
841
|
+
AUTHORING_VALIDATION_STAGE_FIELDS,
|
|
842
|
+
`canonical schema 2.1 validation.authoring_validation.${stage}`,
|
|
843
|
+
);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
749
846
|
if (hasOwn(spec, 'decisions')) {
|
|
750
847
|
if (!Array.isArray(spec.decisions) || spec.decisions.length === 0) failPrecondition('canonical schema 2.1 decisions must be omitted or a non-empty array');
|
|
751
848
|
for (const [index, decision] of spec.decisions.entries()) {
|
|
@@ -790,7 +887,7 @@ function assertLegacyMigrationClosedWorld(spec) {
|
|
|
790
887
|
rejectUnknownKeys(spec.scope_lock, SCOPE_LOCK_FIELDS, 'schema 2.0 scope_lock');
|
|
791
888
|
rejectUnknownKeys(spec.authoring, AUTHORING_FIELDS, 'schema 2.0 authoring');
|
|
792
889
|
rejectUnknownKeys(spec.coordination, LEGACY_COORDINATION_FIELDS, 'schema 2.0 coordination');
|
|
793
|
-
rejectUnknownKeys(spec.validation,
|
|
890
|
+
rejectUnknownKeys(spec.validation, LEGACY_VALIDATION_FIELDS, 'schema 2.0 validation');
|
|
794
891
|
rejectUnknownKeys(spec.validation?.semantic_review, LEGACY_SEMANTIC_REVIEW_FIELDS, 'schema 2.0 validation.semantic_review');
|
|
795
892
|
for (const [index, counterexample] of (spec.validation?.semantic_review?.counterexamples || []).entries()) {
|
|
796
893
|
rejectUnknownKeys(counterexample, ['criterion', 'case_kind', 'scenario', 'expected', 'decision_refs', 'verification_ref'], `schema 2.0 semantic_review.counterexamples[${index}]`);
|
|
@@ -993,11 +1090,36 @@ function validateRegistryEntry(taskPath, entry, declaredSet) {
|
|
|
993
1090
|
}
|
|
994
1091
|
|
|
995
1092
|
function semanticReviewShapeValid(review) {
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1093
|
+
if (!isPlainObject(review) || Object.keys(review).sort().join(',') !== [...SEMANTIC_REVIEW_FIELDS].sort().join(',')) return false;
|
|
1094
|
+
if (!['not-run', 'completed'].includes(review.status)) return false;
|
|
1095
|
+
if (review.semantic_digest !== null && typeof review.semantic_digest !== 'string') return false;
|
|
1096
|
+
if (review.verdict !== null && !['PASS', 'FAIL'].includes(review.verdict)) return false;
|
|
1097
|
+
if (review.lifecycle_disposition !== null && !['CONTINUE', 'BLOCKED'].includes(review.lifecycle_disposition)) return false;
|
|
1098
|
+
if (!Array.isArray(review.findings) || !review.findings.every((finding) => isPlainObject(finding)
|
|
1099
|
+
&& Object.keys(finding).sort().join(',') === [...SEMANTIC_REVIEW_FINDING_FIELDS].sort().join(','))) return false;
|
|
1100
|
+
if (!Array.isArray(review.unresolved_decisions) || !review.unresolved_decisions.every((decision) => isPlainObject(decision)
|
|
1101
|
+
&& Object.keys(decision).sort().join(',') === [...SEMANTIC_REVIEW_DECISION_FIELDS].sort().join(','))) return false;
|
|
1102
|
+
if (!Array.isArray(review.graph_coverage) || !review.graph_coverage.every((coverage) => isPlainObject(coverage)
|
|
1103
|
+
&& Object.keys(coverage).sort().join(',') === [...SEMANTIC_REVIEW_COVERAGE_FIELDS].sort().join(','))) return false;
|
|
1104
|
+
if (!Number.isInteger(review.repair_round) || review.repair_round < 0 || review.repair_round > 2) return false;
|
|
1105
|
+
if (!Array.isArray(review.reviewed_criteria) || !Array.isArray(review.counterexamples)) return false;
|
|
1106
|
+
if (review.reviewer_evidence !== null && (!isPlainObject(review.reviewer_evidence)
|
|
1107
|
+
|| Object.keys(review.reviewer_evidence).sort().join(',') !== [...SEMANTIC_REVIEW_EVIDENCE_FIELDS].sort().join(','))) return false;
|
|
1108
|
+
return true;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
function semanticReviewHistoryShapeValid(history) {
|
|
1112
|
+
if (!isPlainObject(history) || Object.keys(history).sort().join(',') !== [...SEMANTIC_REVIEW_HISTORY_FIELDS].sort().join(',')) return false;
|
|
1113
|
+
if (typeof history.lineage_id !== 'string') return false;
|
|
1114
|
+
return Array.isArray(history.entries) && history.entries.every((entry) => isPlainObject(entry)
|
|
1115
|
+
&& Object.keys(entry).sort().join(',') === [...SEMANTIC_REVIEW_HISTORY_ENTRY_FIELDS].sort().join(','));
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
function authoringValidationShapeValid(receipt) {
|
|
1119
|
+
if (receipt === null) return true;
|
|
1120
|
+
if (!isPlainObject(receipt) || Object.keys(receipt).sort().join(',') !== [...AUTHORING_VALIDATION_FIELDS].sort().join(',')) return false;
|
|
1121
|
+
return ['requirements', 'design', 'research', 'tasks'].every((stage) => isPlainObject(receipt[stage])
|
|
1122
|
+
&& Object.keys(receipt[stage]).sort().join(',') === [...AUTHORING_VALIDATION_STAGE_FIELDS].sort().join(','));
|
|
1001
1123
|
}
|
|
1002
1124
|
|
|
1003
1125
|
function preflightSpec21Candidate(spec, evidence) {
|
|
@@ -1047,7 +1169,9 @@ function preflightSpec21Candidate(spec, evidence) {
|
|
|
1047
1169
|
failPrecondition('schema 2.1 candidate coordination.boundaries must be an array');
|
|
1048
1170
|
}
|
|
1049
1171
|
if (!isPlainObject(spec.validation) || !VALIDATION_STATUS_VALUES.has(spec.validation.status)
|
|
1050
|
-
|| !semanticReviewShapeValid(spec.validation.semantic_review)
|
|
1172
|
+
|| !semanticReviewShapeValid(spec.validation.semantic_review)
|
|
1173
|
+
|| !semanticReviewHistoryShapeValid(spec.validation.semantic_review_history)
|
|
1174
|
+
|| !authoringValidationShapeValid(spec.validation.authoring_validation)) {
|
|
1051
1175
|
failPrecondition('schema 2.1 candidate validation state is invalid');
|
|
1052
1176
|
}
|
|
1053
1177
|
if (typeof spec.ready_for_implementation !== 'boolean') failPrecondition('schema 2.1 candidate readiness must be boolean');
|
|
@@ -1122,19 +1246,48 @@ function normalizeSpec21Candidate(candidate, evidence) {
|
|
|
1122
1246
|
failPrecondition('existing scope_lock canonical fields are invalid');
|
|
1123
1247
|
}
|
|
1124
1248
|
|
|
1125
|
-
|
|
1249
|
+
// D13/I21/R3.9: spec-scaffold.cjs never *writes* `validated` for any
|
|
1250
|
+
// authoring field — only spec-authoring-validation.cjs's atomic coordinator
|
|
1251
|
+
// may flip a stage to validated, after its own clean validator+grounder
|
|
1252
|
+
// pass. It does, however, carry an existing `validated` reading forward
|
|
1253
|
+
// untouched while that stage's C16 receipt still matches the stage's
|
|
1254
|
+
// current bytes, exactly as it already preserves the C13 history and the
|
|
1255
|
+
// receipt itself below: a still-fresh durable record is not scaffold's to
|
|
1256
|
+
// destroy, and drafting it would force a pointless re-validation. Any
|
|
1257
|
+
// stage whose bytes actually changed no longer matches its receipt and
|
|
1258
|
+
// therefore lands on draft here (and reads as draft everywhere via I20).
|
|
1259
|
+
const receiptForFreshness = authoringValidationShapeValid(candidate.validation?.authoring_validation)
|
|
1260
|
+
? candidate.validation.authoring_validation
|
|
1261
|
+
: null;
|
|
1262
|
+
// One pass over the current bytes for all four stages; assertAuthoringFresh
|
|
1263
|
+
// already treats a non-`validated` enum as not fresh. Any unreadable
|
|
1264
|
+
// artifact fails closed to draft here, and the per-stage preconditions
|
|
1265
|
+
// below still report the missing file as the real error.
|
|
1266
|
+
let authoringFreshness = null;
|
|
1267
|
+
if (receiptForFreshness !== null && typeof evidence.specDir === 'string') {
|
|
1268
|
+
try {
|
|
1269
|
+
authoringFreshness = AUTHORING_DIGEST.assertAuthoringFresh({
|
|
1270
|
+
specDir: evidence.specDir,
|
|
1271
|
+
authoring: candidate.authoring,
|
|
1272
|
+
receipt: receiptForFreshness,
|
|
1273
|
+
fields: ['requirements', 'design', 'research', 'tasks'],
|
|
1274
|
+
});
|
|
1275
|
+
} catch {
|
|
1276
|
+
authoringFreshness = null;
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
const carryForward = (field) => (authoringFreshness?.[field]?.fresh ? 'validated' : 'draft');
|
|
1126
1280
|
const authoring = {};
|
|
1127
1281
|
for (const field of ['requirements', 'design']) {
|
|
1128
1282
|
if (!evidence[`${field}Exists`]) failPrecondition(`physical ${field}.md is required for schema 2.1 migration`);
|
|
1129
|
-
authoring[field] =
|
|
1283
|
+
authoring[field] = carryForward(field);
|
|
1130
1284
|
}
|
|
1131
1285
|
if (evidence.researchExists) {
|
|
1132
1286
|
const pointer = isPlainObject(candidate.research) ? candidate.research.path : candidate.research;
|
|
1133
1287
|
if (pointer !== undefined && pointer !== null && pointer !== 'research.md') {
|
|
1134
1288
|
failPrecondition('legacy research pointer is ambiguous; expected canonical research.md');
|
|
1135
1289
|
}
|
|
1136
|
-
authoring.research =
|
|
1137
|
-
? 'validated' : 'draft';
|
|
1290
|
+
authoring.research = carryForward('research');
|
|
1138
1291
|
} else {
|
|
1139
1292
|
if (hasOwn(candidate, 'research') && candidate.research !== null) {
|
|
1140
1293
|
failPrecondition('research pointer exists without a physical research.md artifact');
|
|
@@ -1142,8 +1295,7 @@ function normalizeSpec21Candidate(candidate, evidence) {
|
|
|
1142
1295
|
authoring.research = 'absent';
|
|
1143
1296
|
}
|
|
1144
1297
|
if (evidence.taskFiles.length > 0) {
|
|
1145
|
-
authoring.tasks =
|
|
1146
|
-
? 'validated' : 'draft';
|
|
1298
|
+
authoring.tasks = carryForward('tasks');
|
|
1147
1299
|
} else {
|
|
1148
1300
|
authoring.tasks = 'absent';
|
|
1149
1301
|
}
|
|
@@ -1156,10 +1308,35 @@ function normalizeSpec21Candidate(candidate, evidence) {
|
|
|
1156
1308
|
if (hasOwn(entry, key)) projectedRegistry[taskPath][key] = cloneJson(entry[key]);
|
|
1157
1309
|
}
|
|
1158
1310
|
}
|
|
1311
|
+
// semantic_review_history (C13) and authoring_validation (C16) are durable
|
|
1312
|
+
// records this migration step never wipes on its own: history is
|
|
1313
|
+
// append-only for the life of the spec record (I6 — lineage_id never
|
|
1314
|
+
// resets on an ordinary content edit) and the C16 receipt may only ever be
|
|
1315
|
+
// written by spec-authoring-validation.cjs (I21) — scaffold is not a
|
|
1316
|
+
// writer of either and only seeds a fresh skeleton when a pre-cutover
|
|
1317
|
+
// legacy spec never carried them at all.
|
|
1318
|
+
const historyValid = hasOwn(candidate.validation || {}, 'semantic_review_history')
|
|
1319
|
+
&& semanticReviewHistoryShapeValid(candidate.validation.semantic_review_history);
|
|
1320
|
+
const authoringValidationValid = hasOwn(candidate.validation || {}, 'authoring_validation')
|
|
1321
|
+
&& authoringValidationShapeValid(candidate.validation.authoring_validation);
|
|
1159
1322
|
const validationValid = isPlainObject(candidate.validation)
|
|
1160
1323
|
&& VALIDATION_STATUS_VALUES.has(candidate.validation.status)
|
|
1161
|
-
&& semanticReviewShapeValid(candidate.validation.semantic_review)
|
|
1324
|
+
&& semanticReviewShapeValid(candidate.validation.semantic_review)
|
|
1325
|
+
&& historyValid
|
|
1326
|
+
&& authoringValidationValid;
|
|
1162
1327
|
const readinessValid = typeof candidate.ready_for_implementation === 'boolean';
|
|
1328
|
+
const preservedHistory = historyValid
|
|
1329
|
+
? cloneJson(candidate.validation.semantic_review_history)
|
|
1330
|
+
: semanticReviewHistoryInitial(featureName, candidate.created_at);
|
|
1331
|
+
const preservedAuthoringValidation = authoringValidationValid
|
|
1332
|
+
? cloneJson(candidate.validation.authoring_validation)
|
|
1333
|
+
: null;
|
|
1334
|
+
const freshValidation = {
|
|
1335
|
+
status: 'not-run',
|
|
1336
|
+
semantic_review: semanticReviewNotRun(),
|
|
1337
|
+
semantic_review_history: preservedHistory,
|
|
1338
|
+
authoring_validation: preservedAuthoringValidation,
|
|
1339
|
+
};
|
|
1163
1340
|
const next = {
|
|
1164
1341
|
schema_version: '2.1',
|
|
1165
1342
|
feature_name: featureName,
|
|
@@ -1170,9 +1347,7 @@ function normalizeSpec21Candidate(candidate, evidence) {
|
|
|
1170
1347
|
scope_lock: scopeLock,
|
|
1171
1348
|
authoring,
|
|
1172
1349
|
coordination: cloneJson(evidence.coordination),
|
|
1173
|
-
validation: validationValid ? cloneJson(candidate.validation) :
|
|
1174
|
-
status: 'not-run', semantic_review: semanticReviewNotRun(),
|
|
1175
|
-
},
|
|
1350
|
+
validation: validationValid ? cloneJson(candidate.validation) : freshValidation,
|
|
1176
1351
|
semantic_model: isLegacy ? null : cloneJson(candidate.semantic_model),
|
|
1177
1352
|
ready_for_implementation: readinessValid ? candidate.ready_for_implementation : false,
|
|
1178
1353
|
workflow_policy: cloneJson(evidence.workflowPolicy),
|
|
@@ -1185,7 +1360,7 @@ function normalizeSpec21Candidate(candidate, evidence) {
|
|
|
1185
1360
|
|| isLegacy || JSON.stringify(next) !== JSON.stringify(candidate);
|
|
1186
1361
|
if (semanticChanged || !validationValid || !readinessValid) {
|
|
1187
1362
|
next.ready_for_implementation = false;
|
|
1188
|
-
next.validation =
|
|
1363
|
+
next.validation = freshValidation;
|
|
1189
1364
|
next.semantic_model = null;
|
|
1190
1365
|
}
|
|
1191
1366
|
const changed = JSON.stringify(next) !== JSON.stringify(candidate);
|
|
@@ -1719,6 +1894,7 @@ function scaffoldTasksOnly({ args, specDir, specPath, spec, persistedPolicy, par
|
|
|
1719
1894
|
const normalized = normalizeSpec21Candidate(nextSpec, {
|
|
1720
1895
|
featureName: args.feature,
|
|
1721
1896
|
timestamp: ts,
|
|
1897
|
+
specDir,
|
|
1722
1898
|
requirementsExists: fs.existsSync(requirementsPath),
|
|
1723
1899
|
designExists: fs.existsSync(designPath),
|
|
1724
1900
|
researchExists: researchExists || researchTemplate !== null,
|
|
@@ -1775,11 +1951,16 @@ function syncSemanticModel({ workRoot, args, specDir, ts }) {
|
|
|
1775
1951
|
try { spec = JSON.parse(fs.readFileSync(specPath, 'utf8')); }
|
|
1776
1952
|
catch (error) { failPrecondition(`--sync-semantic-model spec.json is invalid JSON (${error.message})`); }
|
|
1777
1953
|
if (!isPlainObject(spec) || spec.schema_version !== '2.1') failPrecondition('--sync-semantic-model accepts canonical schema 2.1 only');
|
|
1778
|
-
assertCanonicalSpecInputClosedWorld(spec);
|
|
1954
|
+
assertCanonicalSpecInputClosedWorld(spec, { allowLegacyReview: true });
|
|
1779
1955
|
const projection = SEMANTIC.modelFromMarkdown(specDir, spec);
|
|
1780
1956
|
if (projection.errors.length) failPrecondition(`semantic projection is invalid (${projection.errors.join('; ')})`);
|
|
1781
1957
|
const modelUnchanged = SEMANTIC.stableJson(spec.semantic_model) === SEMANTIC.stableJson(projection.model);
|
|
1782
|
-
|
|
1958
|
+
const lifecycleCanonical = isPlainObject(spec.validation)
|
|
1959
|
+
&& Object.keys(spec.validation).sort().join(',') === [...VALIDATION_FIELDS].sort().join(',')
|
|
1960
|
+
&& semanticReviewShapeValid(spec.validation.semantic_review)
|
|
1961
|
+
&& semanticReviewHistoryShapeValid(spec.validation.semantic_review_history)
|
|
1962
|
+
&& authoringValidationShapeValid(spec.validation.authoring_validation);
|
|
1963
|
+
if (modelUnchanged && lifecycleCanonical) {
|
|
1783
1964
|
console.log(`SEMANTIC_MODEL_UNCHANGED ${path.relative(process.cwd(), specDir) || specDir}`);
|
|
1784
1965
|
return;
|
|
1785
1966
|
}
|
|
@@ -1787,7 +1968,26 @@ function syncSemanticModel({ workRoot, args, specDir, ts }) {
|
|
|
1787
1968
|
next.semantic_model = projection.model;
|
|
1788
1969
|
next.updated_at = ts;
|
|
1789
1970
|
next.ready_for_implementation = false;
|
|
1790
|
-
|
|
1971
|
+
if (!lifecycleCanonical) {
|
|
1972
|
+
for (const stage of ['requirements', 'design', 'research', 'tasks']) {
|
|
1973
|
+
if (next.authoring?.[stage] === 'validated') next.authoring[stage] = 'draft';
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
// Only the ephemeral semantic_review pointer is invalidated by a projection
|
|
1977
|
+
// resync; semantic_review_history (C13) and authoring_validation (C16) are
|
|
1978
|
+
// durable records this command never writes and must carry forward as-is.
|
|
1979
|
+
next.validation = {
|
|
1980
|
+
status: 'not-run',
|
|
1981
|
+
semantic_review: semanticReviewNotRun(),
|
|
1982
|
+
semantic_review_history: hasOwn(spec.validation, 'semantic_review_history')
|
|
1983
|
+
&& semanticReviewHistoryShapeValid(spec.validation.semantic_review_history)
|
|
1984
|
+
? cloneJson(spec.validation.semantic_review_history)
|
|
1985
|
+
: semanticReviewHistoryInitial(args.feature, spec.created_at),
|
|
1986
|
+
authoring_validation: hasOwn(spec.validation, 'authoring_validation')
|
|
1987
|
+
&& authoringValidationShapeValid(spec.validation.authoring_validation)
|
|
1988
|
+
? cloneJson(spec.validation.authoring_validation)
|
|
1989
|
+
: null,
|
|
1990
|
+
};
|
|
1791
1991
|
preflightSpec21Candidate(next, {
|
|
1792
1992
|
featureName: args.feature,
|
|
1793
1993
|
requirementsExists: fs.existsSync(path.join(specDir, 'requirements.md')),
|
|
@@ -1807,7 +2007,45 @@ function syncSemanticModel({ workRoot, args, specDir, ts }) {
|
|
|
1807
2007
|
console.log(`SEMANTIC_MODEL_SYNCED ${path.relative(process.cwd(), specDir) || specDir}`);
|
|
1808
2008
|
}
|
|
1809
2009
|
|
|
2010
|
+
// Version lives in a different file depending on where this script runs: an
|
|
2011
|
+
// installed adapter carries `<adapter>/cafekit.json`, the source tree carries
|
|
2012
|
+
// `packages/spec/package.json`. Read them with fs rather than require so the
|
|
2013
|
+
// runtime dependency closure — which may only copy files under src/claude —
|
|
2014
|
+
// stays satisfiable in an installed runtime.
|
|
2015
|
+
function readVersion() {
|
|
2016
|
+
const candidates = [
|
|
2017
|
+
path.resolve(__dirname, '..', 'cafekit.json'),
|
|
2018
|
+
path.resolve(__dirname, '..', '..', '..', 'package.json'),
|
|
2019
|
+
];
|
|
2020
|
+
for (const candidate of candidates) {
|
|
2021
|
+
try {
|
|
2022
|
+
const parsed = JSON.parse(fs.readFileSync(candidate, 'utf8'));
|
|
2023
|
+
if (typeof parsed.version === 'string' && parsed.version) return parsed.version;
|
|
2024
|
+
} catch {
|
|
2025
|
+
// try the next location
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
return 'unknown';
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
// --help/--version answer before any precondition runs: a caller asking what
|
|
2032
|
+
// this command is must not be told their help flag is an invalid feature name,
|
|
2033
|
+
// and must not trigger transaction recovery as a side effect.
|
|
2034
|
+
function respondToInfoFlags(argv) {
|
|
2035
|
+
const flags = argv.slice(2);
|
|
2036
|
+
if (flags.includes('--help') || flags.includes('-h')) {
|
|
2037
|
+
console.log(USAGE_TEXT);
|
|
2038
|
+
return true;
|
|
2039
|
+
}
|
|
2040
|
+
if (flags.includes('--version') || flags.includes('-v')) {
|
|
2041
|
+
console.log(readVersion());
|
|
2042
|
+
return true;
|
|
2043
|
+
}
|
|
2044
|
+
return false;
|
|
2045
|
+
}
|
|
2046
|
+
|
|
1810
2047
|
function main() {
|
|
2048
|
+
if (respondToInfoFlags(process.argv)) return;
|
|
1811
2049
|
const workRoot = path.resolve(process.cwd());
|
|
1812
2050
|
const recoveredTransactions = recoverTransactions(workRoot);
|
|
1813
2051
|
if (recoveredTransactions > 0) {
|
|
@@ -1986,6 +2224,8 @@ function main() {
|
|
|
1986
2224
|
Object.assign(spec, POLICY.persistWorkflowPolicySnapshot(spec, policyInput));
|
|
1987
2225
|
spec.validation = isPlainObject(spec.validation) ? spec.validation : {};
|
|
1988
2226
|
spec.validation.semantic_review = semanticReviewNotRun();
|
|
2227
|
+
spec.validation.semantic_review_history = semanticReviewHistoryInitial(args.feature, ts);
|
|
2228
|
+
spec.validation.authoring_validation = null;
|
|
1989
2229
|
if (args.research) {
|
|
1990
2230
|
spec.research = 'research.md';
|
|
1991
2231
|
spec.authoring.research = 'draft';
|