@smartmemory/compose 0.3.5-beta → 0.3.7
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/.claude/skills/compose/SKILL.md +42 -88
- package/bin/compose.js +288 -0
- package/bin/git-hooks/pre-push.template +29 -0
- package/bin/judgment-import.js +572 -0
- package/contracts/feature-json.schema.json +5 -0
- package/contracts/judgment-record.schema.json +821 -0
- package/dist/assets/App-PkZzHeMj.js +894 -0
- package/dist/assets/_baseUniq-Bo837sRJ.js +1 -0
- package/dist/assets/arc-BafGpyqE.js +1 -0
- package/dist/assets/architectureDiagram-Q4EWVU46-BOBfUsqL.js +36 -0
- package/dist/assets/blockDiagram-DXYQGD6D-Dwodev1a.js +132 -0
- package/dist/assets/{browser-BSM23If2.js → browser-1ntj1-x_.js} +6 -6
- package/dist/assets/{c4Diagram-LMCZKHZV-CUmTCrMW.js → c4Diagram-AHTNJAMY-CU_bhYag.js} +1 -1
- package/dist/assets/channel-qVK_qn4E.js +1 -0
- package/dist/assets/{chunk-JWPE2WC7-DQFs7gjy.js → chunk-4BX2VUAB-p8WsDwnO.js} +1 -1
- package/dist/assets/chunk-4TB4RGXK-B8h7-eR0.js +206 -0
- package/dist/assets/{chunk-XXDRQBXY-CSdt06Hp.js → chunk-55IACEB6-DxeEr98s.js} +1 -1
- package/dist/assets/{chunk-VR4S4FIN-Dvhgnm7E.js → chunk-EDXVE4YY-BYt8F151.js} +1 -1
- package/dist/assets/{chunk-5VM5RSS4-Qw-L7OoJ.js → chunk-FMBD7UC4-DGSOVeie.js} +1 -1
- package/dist/assets/chunk-OYMX7WX6-B-QdgYR2.js +231 -0
- package/dist/assets/{chunk-2Q5K7J3B-Cyvi1AFH.js → chunk-QZHKN3VN-Du5UAZLs.js} +1 -1
- package/dist/assets/{chunk-32BRIVSS-B1y5FpOd.js → chunk-YZCP3GAM-C8JbNBSk.js} +1 -1
- package/dist/assets/classDiagram-6PBFFD2Q-B8UcfC1q.js +1 -0
- package/dist/assets/classDiagram-v2-HSJHXN6E-B8UcfC1q.js +1 -0
- package/dist/assets/clone-Pu3RyLUh.js +1 -0
- package/dist/assets/{cose-bilkent-JH36ORCC-G8l05tXy.js → cose-bilkent-S5V4N54A-O1ESaqge.js} +1 -1
- package/dist/assets/dagre-KV5264BT-CPTmFPHw.js +4 -0
- package/dist/assets/diagram-5BDNPKRD-B3PNrWs5.js +10 -0
- package/dist/assets/diagram-G4DWMVQ6-Cscfr6vc.js +24 -0
- package/dist/assets/diagram-MMDJMWI5-CSfqZ-TM.js +43 -0
- package/dist/assets/diagram-TYMM5635-Cg4aYS7W.js +24 -0
- package/dist/assets/erDiagram-SMLLAGMA-_ZqwG5pl.js +85 -0
- package/dist/assets/flowDiagram-DWJPFMVM-C83boxFT.js +162 -0
- package/dist/assets/ganttDiagram-T4ZO3ILL-CWnIjuEi.js +292 -0
- package/dist/assets/gitGraphDiagram-UUTBAWPF-DrMdxZfH.js +106 -0
- package/dist/assets/graph-Bi99_6Yf.js +331 -0
- package/dist/assets/graph-RE4I7Ty7.js +1 -0
- package/dist/assets/index-Rm2RE-c0.js +123 -0
- package/dist/assets/infoDiagram-42DDH7IO-BLmP4Epr.js +2 -0
- package/dist/assets/{ishikawaDiagram-FXEZZL3T-Hm4zSDlM.js → ishikawaDiagram-UXIWVN3A-yuWWshKN.js} +5 -5
- package/dist/assets/{journeyDiagram-5HDEW3XC-CIqcTFuO.js → journeyDiagram-VCZTEJTY-BOfhaJov.js} +1 -1
- package/dist/assets/{kanban-definition-HUTT4EX6-CB9jCXj_.js → kanban-definition-6JOO6SKY-Bbolde15.js} +7 -7
- package/dist/assets/katex-DkKDou_j.js +257 -0
- package/dist/assets/layout-BSf33zm8.js +1 -0
- package/dist/assets/{linear-CsXqv70E.js → linear-AvSTWMqx.js} +1 -1
- package/dist/assets/min-QBM8H4xN.js +1 -0
- package/dist/assets/{mindmap-definition-LN4V7U3C-CpqH5F_P.js → mindmap-definition-QFDTVHPH-BuvgtqIc.js} +7 -7
- package/dist/assets/{mobile-CaoXUwAr.js → mobile-BnXEOE3U.js} +2 -2
- package/dist/assets/pieDiagram-DEJITSTG-DIzF16vh.js +30 -0
- package/dist/assets/quadrantDiagram-34T5L4WZ-D-mbUIjS.js +7 -0
- package/dist/assets/{requirementDiagram-TGXJPOKE-D1lG1P7h.js → requirementDiagram-MS252O5E-CEs4kCLd.js} +3 -3
- package/dist/assets/sankeyDiagram-XADWPNL6-DFsnCr9n.js +10 -0
- package/dist/assets/sequenceDiagram-FGHM5R23-BEJYdTjQ.js +157 -0
- package/dist/assets/stateDiagram-FHFEXIEX-BBXs57uY.js +1 -0
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-BqKuX4rj.js +1 -0
- package/dist/assets/{timeline-definition-FHXFAJF6-CzqqUDvt.js → timeline-definition-GMOUNBTQ-BGvLoVAY.js} +3 -3
- package/dist/assets/vennDiagram-DHZGUBPP-9LaBTMe0.js +34 -0
- package/dist/assets/wardley-RL74JXVD-P4MEqMTP.js +162 -0
- package/dist/assets/wardleyDiagram-NUSXRM2D-o-tmxnlC.js +20 -0
- package/dist/assets/xychartDiagram-5P7HB3ND-Dpn7V6qk.js +7 -0
- package/dist/index.html +2 -2
- package/lib/bug-escalation.js +30 -4
- package/lib/build.js +777 -52
- package/lib/canon-guard.js +223 -0
- package/lib/canon-registry.js +187 -0
- package/lib/codex-preflight.js +26 -4
- package/lib/dispatch-ledger.js +301 -0
- package/lib/dispatch-metrics.js +236 -0
- package/lib/experiment-judge.js +6 -1
- package/lib/feature-writer.js +9 -0
- package/lib/gsd.js +11 -2
- package/lib/hooks-status.js +32 -3
- package/lib/idempotency.js +33 -8
- package/lib/judgment/schema.js +23 -0
- package/lib/judgment/store/index.js +207 -0
- package/lib/judgment/store/records.js +405 -0
- package/lib/judgment-attest.js +259 -0
- package/lib/judgment-gen.js +691 -0
- package/lib/judgment-verify.js +153 -0
- package/lib/judgment-write-guard.js +230 -0
- package/lib/judgment-writer.js +3403 -0
- package/lib/lane-gate.js +2 -0
- package/lib/lifecycle-modes.js +42 -0
- package/lib/local-claude-connector.js +199 -54
- package/lib/mcp-enforcement.js +21 -35
- package/lib/result-normalizer.js +93 -15
- package/lib/review-normalize.js +4 -0
- package/lib/stratum-mcp-client.js +131 -6
- package/package.json +2 -2
- package/server/compose-mcp-tools.js +49 -1
- package/server/compose-mcp.js +220 -0
- package/server/mcp-tool-policy.js +1 -0
- package/dist/assets/App-BG92y_CY.js +0 -896
- package/dist/assets/abnfDiagram-VRR7QNED-Bip6cMjP.js +0 -1
- package/dist/assets/arc-CyMTM07c.js +0 -1
- package/dist/assets/architectureDiagram-ZJ3FMSHR-BukV0CJw.js +0 -36
- package/dist/assets/blockDiagram-677ZJIJ3-Bjb_MNB4.js +0 -132
- package/dist/assets/channel-BzkmtaLy.js +0 -1
- package/dist/assets/chunk-EX3LRPZG-G_MZEji_.js +0 -231
- package/dist/assets/chunk-MOJQB5TN-CQZQYMcz.js +0 -88
- package/dist/assets/chunk-RYQCIY6F-CmAda4sZ.js +0 -1
- package/dist/assets/chunk-V7JOEXUC-CbjBz-Ek.js +0 -206
- package/dist/assets/classDiagram-OUVF2IWQ-BIvLcbex.js +0 -1
- package/dist/assets/classDiagram-v2-EOCWNBFH-BIvLcbex.js +0 -1
- package/dist/assets/cynefin-VYW2F7L2-C3n1PNJC.js +0 -178
- package/dist/assets/cynefinDiagram-TSTJHNR4-C8N_CvKJ.js +0 -62
- package/dist/assets/dagre-VKFMJZFB-B2pkwDhN.js +0 -4
- package/dist/assets/diagram-FQU43EPY-DPHD_t-1.js +0 -3
- package/dist/assets/diagram-G47NLZAW-DGmHIZRr.js +0 -24
- package/dist/assets/diagram-NH7WQ7WH-BlLWwKVt.js +0 -24
- package/dist/assets/diagram-OA4YK3LP-DEv704Re.js +0 -30
- package/dist/assets/diagram-WEI45ONY-Bj6PGUCe.js +0 -41
- package/dist/assets/ebnfDiagram-CCIWWBDH-Ch-icksT.js +0 -1
- package/dist/assets/erDiagram-Q63AITRT-CI3lwEFA.js +0 -85
- package/dist/assets/flowDiagram-23GEKE2U-DcUANpAz.js +0 -156
- package/dist/assets/ganttDiagram-NO4QXBWP-Cy0Ie8KS.js +0 -292
- package/dist/assets/gitGraphDiagram-IHSO6WYX-BswI52az.js +0 -106
- package/dist/assets/graph-BXPQrYYB.js +0 -331
- package/dist/assets/graph-C9eacEi8.js +0 -1
- package/dist/assets/index-B1H7-G2y.js +0 -119
- package/dist/assets/infoDiagram-FWYZ7A6U-BBunqvr-.js +0 -2
- package/dist/assets/katex-C5jXJg4s.js +0 -257
- package/dist/assets/layout-DEXfKzaS.js +0 -1
- package/dist/assets/map-Czzmt4hB.js +0 -1
- package/dist/assets/pegDiagram-2B236MQR-HpQyrDcm.js +0 -1
- package/dist/assets/pieDiagram-ENE6RG2P-xkcpHwnY.js +0 -39
- package/dist/assets/quadrantDiagram-ABIIQ3AL-kebZIOmu.js +0 -7
- package/dist/assets/railroadDiagram-RFXS5EU6-DNvGwWm9.js +0 -1
- package/dist/assets/sankeyDiagram-HTMAVEWB-DOEEcn52.js +0 -40
- package/dist/assets/sequenceDiagram-DBY2YBRQ-DynIVCk_.js +0 -162
- package/dist/assets/sizeCapture-X5ZJPWSS-DJXFjUUL.js +0 -1
- package/dist/assets/stateDiagram-2N3HPSRC-OZ7trFh5.js +0 -1
- package/dist/assets/stateDiagram-v2-6OUMAXLB-GcPRHAJT.js +0 -1
- package/dist/assets/swimlanes-5IMT3BWC-DULYAFmB.js +0 -2
- package/dist/assets/swimlanesDiagram-G3AALYLV-BB0gCy01.js +0 -8
- package/dist/assets/vennDiagram-L72KCM5P-CdKmtcFL.js +0 -34
- package/dist/assets/wardleyDiagram-EHGQE667-DknGwjCA.js +0 -78
- package/dist/assets/xychartDiagram-FW5EYKEG-DdYyKfQ3.js +0 -7
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Combined drift verification for the judgment canon.
|
|
3
|
+
*
|
|
4
|
+
* The inventory, projection roundtrip, and record attestation are evaluated
|
|
5
|
+
* under the writer's advisory lock so a legitimate write cannot expose a
|
|
6
|
+
* partially-published canon to the verifier.
|
|
7
|
+
*/
|
|
8
|
+
import {
|
|
9
|
+
existsSync,
|
|
10
|
+
lstatSync,
|
|
11
|
+
readdirSync,
|
|
12
|
+
} from 'node:fs';
|
|
13
|
+
import { join, posix } from 'node:path';
|
|
14
|
+
|
|
15
|
+
import { readManifest, recordFileSet, verifyRecords } from './judgment-attest.js';
|
|
16
|
+
import {
|
|
17
|
+
checkProjectionRoundtrip,
|
|
18
|
+
generateFromRecords,
|
|
19
|
+
loadSnapshot,
|
|
20
|
+
} from './judgment-gen.js';
|
|
21
|
+
import { withJudgmentLock } from './judgment-writer.js';
|
|
22
|
+
|
|
23
|
+
const JUDGMENT_REL = 'docs/judgment';
|
|
24
|
+
const MANIFEST_REL = `${JUDGMENT_REL}/.attest.json`;
|
|
25
|
+
const RECORDS_REL = `${JUDGMENT_REL}/records`;
|
|
26
|
+
const STRUCTURAL_DIRS = [
|
|
27
|
+
RECORDS_REL,
|
|
28
|
+
`${JUDGMENT_REL}/people`,
|
|
29
|
+
`${JUDGMENT_REL}/positions`,
|
|
30
|
+
];
|
|
31
|
+
// The legitimate depth-1 dirs under records/. Anything else there (e.g. a
|
|
32
|
+
// Bash-created `records/garbage/`) is drift. Deeper legitimate dirs — such as
|
|
33
|
+
// `records/positions/<slug>/` — are admitted by expectedTree's parent-walk over
|
|
34
|
+
// the real record files, so this list stays exact rather than blanket-exempting
|
|
35
|
+
// every nested path.
|
|
36
|
+
const RECORD_STRUCTURAL_DIRS = [
|
|
37
|
+
'goal', 'intents', 'joints', 'people', 'positions', 'predictions', 'situation',
|
|
38
|
+
].map((name) => `${RECORDS_REL}/${name}`);
|
|
39
|
+
|
|
40
|
+
function expectedTree(cwd) {
|
|
41
|
+
// Derive the expected projection set from the SAME snapshot builder that
|
|
42
|
+
// checkProjectionRoundtrip uses (loadSnapshot, exported from judgment-gen).
|
|
43
|
+
// Reconstructing it here would be a mirror that silently diverges into false
|
|
44
|
+
// tree drift the moment loadSnapshot changes.
|
|
45
|
+
const projections = Object.keys(generateFromRecords(loadSnapshot(cwd)));
|
|
46
|
+
const records = recordFileSet(cwd);
|
|
47
|
+
// The manifest is NOT in this set: it lives at .compose/judgment-attest.json,
|
|
48
|
+
// outside the tree being inventoried.
|
|
49
|
+
const files = new Set([...projections, ...records]);
|
|
50
|
+
const directories = new Set([JUDGMENT_REL, ...STRUCTURAL_DIRS, ...RECORD_STRUCTURAL_DIRS]);
|
|
51
|
+
|
|
52
|
+
for (const relPath of files) {
|
|
53
|
+
let parent = posix.dirname(relPath);
|
|
54
|
+
while (parent.startsWith(`${JUDGMENT_REL}/`)) {
|
|
55
|
+
directories.add(parent);
|
|
56
|
+
parent = posix.dirname(parent);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return { files, directories };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function inventoryTree(cwd, expected) {
|
|
64
|
+
const root = join(cwd, ...JUDGMENT_REL.split('/'));
|
|
65
|
+
if (!existsSync(root)) return [];
|
|
66
|
+
|
|
67
|
+
const rootStat = lstatSync(root);
|
|
68
|
+
if (rootStat.isSymbolicLink()) {
|
|
69
|
+
return [{ path: JUDGMENT_REL, kind: 'symlink' }];
|
|
70
|
+
}
|
|
71
|
+
if (!rootStat.isDirectory()) {
|
|
72
|
+
return [{ path: JUDGMENT_REL, kind: 'unexpected' }];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const drift = [];
|
|
76
|
+
|
|
77
|
+
function walk(absDir, relDir) {
|
|
78
|
+
const entries = readdirSync(absDir)
|
|
79
|
+
.sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
|
|
80
|
+
|
|
81
|
+
for (const name of entries) {
|
|
82
|
+
const absPath = join(absDir, name);
|
|
83
|
+
const relPath = `${relDir}/${name}`;
|
|
84
|
+
const stat = lstatSync(absPath);
|
|
85
|
+
|
|
86
|
+
if (stat.isSymbolicLink()) {
|
|
87
|
+
drift.push({ path: relPath, kind: 'symlink' });
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (stat.isDirectory()) {
|
|
92
|
+
// An EMPTY unexpected dir is not reported: it holds no canon, and
|
|
93
|
+
// UndoLog.restore unlinks files without removing their directories, so a
|
|
94
|
+
// rolled-back op legitimately leaves e.g. an empty positions/<slug>/.
|
|
95
|
+
// Flagging those would false-RED on correct behaviour. A non-empty stray
|
|
96
|
+
// dir is still caught — every file inside it fails the file rule below.
|
|
97
|
+
if (!expected.directories.has(relPath) && readdirSync(absPath).length > 0) {
|
|
98
|
+
drift.push({ path: relPath, kind: 'unexpected' });
|
|
99
|
+
}
|
|
100
|
+
walk(absPath, relPath);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (!stat.isFile() || !expected.files.has(relPath)) {
|
|
105
|
+
drift.push({ path: relPath, kind: 'unexpected' });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
walk(root, JUDGMENT_REL);
|
|
111
|
+
drift.sort((a, b) => {
|
|
112
|
+
if (a.path !== b.path) return a.path < b.path ? -1 : 1;
|
|
113
|
+
return a.kind < b.kind ? -1 : a.kind > b.kind ? 1 : 0;
|
|
114
|
+
});
|
|
115
|
+
return drift;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* True when this project has no judgment canon at all — no `docs/judgment/`
|
|
120
|
+
* tree and no attestation baseline. There is nothing to verify, so verification
|
|
121
|
+
* must pass: most Compose projects never adopt the judgment layer, and a
|
|
122
|
+
* gate that fails them would break every pre-push and ship in those repos.
|
|
123
|
+
*
|
|
124
|
+
* This is NOT the same as a deleted canon. The baseline lives outside
|
|
125
|
+
* `docs/judgment/**` (at `.compose/judgment-attest.json`), so wiping the canon
|
|
126
|
+
* leaves the manifest behind — `hasNoCanon` is false and every record it
|
|
127
|
+
* remembers surfaces as `removed` drift.
|
|
128
|
+
*/
|
|
129
|
+
function hasNoCanon(cwd) {
|
|
130
|
+
return !existsSync(join(cwd, ...JUDGMENT_REL.split('/')))
|
|
131
|
+
&& readManifest(cwd) === null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export async function verifyJudgmentCanon(cwd) {
|
|
135
|
+
return withJudgmentLock(cwd, async () => {
|
|
136
|
+
if (hasNoCanon(cwd)) {
|
|
137
|
+
return { ok: true, treeDrift: [], projectionDrift: [], recordDrift: [] };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const treeDrift = inventoryTree(cwd, expectedTree(cwd));
|
|
141
|
+
const projection = checkProjectionRoundtrip(cwd);
|
|
142
|
+
const records = verifyRecords(cwd);
|
|
143
|
+
const projectionDrift = projection.diffs;
|
|
144
|
+
const recordDrift = records.drift;
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
ok: treeDrift.length === 0 && projection.fixedPoint && records.ok,
|
|
148
|
+
treeDrift,
|
|
149
|
+
projectionDrift,
|
|
150
|
+
recordDrift,
|
|
151
|
+
};
|
|
152
|
+
});
|
|
153
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/judgment-write-guard.js — write-time judgment-record validation (S03).
|
|
3
|
+
*
|
|
4
|
+
* Pure leaf validator in the feature-write-guard shape: schema conformance,
|
|
5
|
+
* grounding/elicitation rules, the design's edge→artifact table, and the
|
|
6
|
+
* method gates. The writer (lib/judgment-writer.js) and MCP surface call
|
|
7
|
+
* into this module; it never imports them — imports are restricted to the
|
|
8
|
+
* Ajv SchemaValidator and the judgment schema loader so the graph stays
|
|
9
|
+
* acyclic (the feature-write-guard.js:20-23 discipline).
|
|
10
|
+
*
|
|
11
|
+
* Rules enforced here (design.md rev 5):
|
|
12
|
+
* - every record validates against contracts/judgment-record.schema.json
|
|
13
|
+
* (MCP inputSchema is advisory, never the enforcement)
|
|
14
|
+
* - grounding: ASSERT requires a structured elicitation block
|
|
15
|
+
* - [owner-locked] is unrepresentable through tools — import or the epic's
|
|
16
|
+
* future override only
|
|
17
|
+
* - every transition edge carries its bound artifact or is refused; there
|
|
18
|
+
* is no free under_test→open edge; dissolution is its own artifact
|
|
19
|
+
* - EXT needs sharpened-or-judgment-dispatch before under_test; STRADDLE
|
|
20
|
+
* needs signal+kill-criteria; a SILENT ext result may only yield the
|
|
21
|
+
* joint outcome `inconclusive`
|
|
22
|
+
*/
|
|
23
|
+
import { getJudgmentValidator } from './judgment/schema.js';
|
|
24
|
+
|
|
25
|
+
export class JudgmentWriteValidationError extends Error {
|
|
26
|
+
/**
|
|
27
|
+
* @param {'JUDGMENT_SCHEMA_VIOLATION'|'JUDGMENT_GROUNDING_VIOLATION'|'JUDGMENT_ILLEGAL_EDGE'|'JUDGMENT_METHOD_GATE'} kind
|
|
28
|
+
* @param {string[]} violations
|
|
29
|
+
*/
|
|
30
|
+
constructor(kind, violations) {
|
|
31
|
+
super(`${kind}: ${violations.join('; ')}`);
|
|
32
|
+
this.name = 'JudgmentWriteValidationError';
|
|
33
|
+
this.code = kind;
|
|
34
|
+
this.kind = kind;
|
|
35
|
+
this.violations = violations;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function refuse(kind, violations) {
|
|
40
|
+
throw new JudgmentWriteValidationError(kind, violations);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Validate a record against its contract definition. Throws
|
|
45
|
+
* JUDGMENT_SCHEMA_VIOLATION with ajv error paths on mismatch.
|
|
46
|
+
*
|
|
47
|
+
* @param {'position_revision'|'joint'|'prediction'|'ledger_event'|'pending_intent'} defName
|
|
48
|
+
* @param {object} record
|
|
49
|
+
*/
|
|
50
|
+
export function assertValidRecord(defName, record) {
|
|
51
|
+
const { valid, errors } = getJudgmentValidator().validate(defName, record);
|
|
52
|
+
if (valid) return;
|
|
53
|
+
refuse(
|
|
54
|
+
'JUDGMENT_SCHEMA_VIOLATION',
|
|
55
|
+
(errors || []).map((e) => `${defName}${e.instancePath || ''}: ${e.message}`),
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Grounding rules over a record's claims (position revisions; any record
|
|
61
|
+
* carrying `claims[]`). `via: 'import'` — taken from opts or the record's own
|
|
62
|
+
* provenance — is the only sanctioned owner-tag path in v1.
|
|
63
|
+
*
|
|
64
|
+
* @param {object} record
|
|
65
|
+
* @param {{ via?: 'import' }} [opts]
|
|
66
|
+
*/
|
|
67
|
+
export function assertGrounding(record, opts = {}) {
|
|
68
|
+
const via = opts.via ?? record?.provenance?.via;
|
|
69
|
+
const violations = [];
|
|
70
|
+
for (const claim of (Array.isArray(record?.claims) ? record.claims : [])) {
|
|
71
|
+
if (claim.grounding === 'ASSERT' && !claim.elicitation) {
|
|
72
|
+
violations.push(
|
|
73
|
+
`claim ${claim.id}: grounding ASSERT requires an elicitation block { asked, answered_at, answer_ref } (assert-elicitation-amendment)`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
if (claim.owner_locked === true && via !== 'import') {
|
|
77
|
+
violations.push(
|
|
78
|
+
`claim ${claim.id}: [owner-locked] is unrepresentable through tools — use the importer (via: 'import') or the epic's future override`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (violations.length > 0) refuse('JUDGMENT_GROUNDING_VIOLATION', violations);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The design's edge→artifact table, verbatim. Each legal edge names the
|
|
87
|
+
* exactly-one artifact the transition input must carry; anything else is
|
|
88
|
+
* refused. Keys are `${from}→${to}`.
|
|
89
|
+
*
|
|
90
|
+
* @type {Record<string, (input: object) => string|null>}
|
|
91
|
+
* returns a violation string or null when satisfied
|
|
92
|
+
*/
|
|
93
|
+
const EDGE_TABLE = {
|
|
94
|
+
'open→under_test': () => null, // method gate enforced by assertMethodGate
|
|
95
|
+
'under_test→resolved': (input) =>
|
|
96
|
+
input?.resolution?.outcome === 'resolved'
|
|
97
|
+
? null
|
|
98
|
+
: 'requires resolution { outcome: resolved, evidence }',
|
|
99
|
+
'under_test→inconclusive': (input) =>
|
|
100
|
+
input?.resolution?.outcome === 'inconclusive'
|
|
101
|
+
? null
|
|
102
|
+
: 'requires resolution { outcome: inconclusive, learned, would_have_settled }',
|
|
103
|
+
'under_test→open': (input) =>
|
|
104
|
+
input?.resolution?.outcome === 'failed_to_run'
|
|
105
|
+
? null
|
|
106
|
+
: 'a free under_test→open edge does not exist — requires resolution { outcome: failed_to_run, reason }',
|
|
107
|
+
'open→superseded': (input) =>
|
|
108
|
+
input?.resolution?.outcome === 'superseded'
|
|
109
|
+
? null
|
|
110
|
+
: 'requires resolution { outcome: superseded, why }',
|
|
111
|
+
'under_test→superseded': (input) =>
|
|
112
|
+
input?.resolution?.outcome === 'superseded'
|
|
113
|
+
? null
|
|
114
|
+
: 'requires resolution { outcome: superseded, why }',
|
|
115
|
+
'open→dissolved': (input) =>
|
|
116
|
+
input?.dissolution && !input?.resolution
|
|
117
|
+
? null
|
|
118
|
+
: 'requires dissolution { decomposed_into[] } — its own artifact, not a resolution',
|
|
119
|
+
'under_test→dissolved': (input) =>
|
|
120
|
+
input?.dissolution && !input?.resolution
|
|
121
|
+
? null
|
|
122
|
+
: 'requires dissolution { decomposed_into[] } — its own artifact, not a resolution',
|
|
123
|
+
'resolved→open': (input) =>
|
|
124
|
+
input?.reopen?.shaken_evidence_ref
|
|
125
|
+
? null
|
|
126
|
+
: 'reopen requires { shaken_evidence_ref } (P6)',
|
|
127
|
+
'inconclusive→under_test': (input) => redisposeViolation(input),
|
|
128
|
+
'inconclusive→open': (input) => redisposeViolation(input),
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Re-dispose carries `{ new_resolve_by, new method package }` for BOTH target
|
|
133
|
+
* states (the table is explicit): a gated method may not be re-disposed
|
|
134
|
+
* without its package even when the joint returns to open.
|
|
135
|
+
*/
|
|
136
|
+
function redisposeViolation(input) {
|
|
137
|
+
const redispose = input?.redispose;
|
|
138
|
+
if (!redispose?.new_resolve_by) {
|
|
139
|
+
return 're-dispose requires { new_resolve_by, new method package } (P3 retry-with-different-method)';
|
|
140
|
+
}
|
|
141
|
+
if (redispose.new_resolve_by === 'EXT' && !redispose.ext) {
|
|
142
|
+
return 're-dispose to EXT requires its ext package (sharpened or judgment-dispatch)';
|
|
143
|
+
}
|
|
144
|
+
if (redispose.new_resolve_by === 'STRADDLE' && !redispose.straddle) {
|
|
145
|
+
return 're-dispose to STRADDLE requires straddle { discriminating_signal, kill_criteria }';
|
|
146
|
+
}
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* The legal edge set, exported for the graph-parity contract test (the
|
|
152
|
+
* judgment lifecycle mode and this table must agree edge-for-edge).
|
|
153
|
+
*/
|
|
154
|
+
export const LEGAL_EDGES = Object.freeze(Object.keys(EDGE_TABLE));
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Assert a transition edge is legal and carries its bound artifact.
|
|
158
|
+
*
|
|
159
|
+
* @param {string} from current joint state
|
|
160
|
+
* @param {string} to target joint state
|
|
161
|
+
* @param {object} input transition input ({ resolution?, dissolution?, reopen?, redispose? })
|
|
162
|
+
*/
|
|
163
|
+
export function assertEdgeArtifact(from, to, input = {}) {
|
|
164
|
+
const edge = `${from}→${to}`;
|
|
165
|
+
const rule = EDGE_TABLE[edge];
|
|
166
|
+
if (!rule) {
|
|
167
|
+
refuse('JUDGMENT_ILLEGAL_EDGE', [`edge ${edge} is not in the edge→artifact table — refused`]);
|
|
168
|
+
}
|
|
169
|
+
const violation = rule(input);
|
|
170
|
+
if (violation) refuse('JUDGMENT_ILLEGAL_EDGE', [`edge ${edge}: ${violation}`]);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Method gates (P3 / external-signal contract), applied to the joint as it
|
|
175
|
+
* would stand AFTER the transition (i.e. with any re-disposed method/package
|
|
176
|
+
* already merged by the caller):
|
|
177
|
+
* - EXT → under_test needs `ext` (sharpened package or judgment-dispatch)
|
|
178
|
+
* - STRADDLE → under_test needs `straddle { discriminating_signal, kill_criteria }`
|
|
179
|
+
* - an ext_result of SILENT may only accompany the joint outcome `inconclusive`
|
|
180
|
+
*
|
|
181
|
+
* @param {object} joint the joint record (post-merge view)
|
|
182
|
+
* @param {string} to target state
|
|
183
|
+
* @param {object} [input] transition input (for resolution inspection)
|
|
184
|
+
*/
|
|
185
|
+
export function assertMethodGate(joint, to, input = {}) {
|
|
186
|
+
const violations = [];
|
|
187
|
+
if (to === 'under_test') {
|
|
188
|
+
if (joint.resolve_by === 'EXT' && !joint.ext) {
|
|
189
|
+
violations.push(
|
|
190
|
+
'EXT joint cannot enter under_test without ext { sharpened_question, bar, falsifier } or ext { judgment_dispatch: true, reason } (BAR-OR-JUDGMENT)',
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
if (joint.resolve_by === 'STRADDLE' && !joint.straddle) {
|
|
194
|
+
violations.push(
|
|
195
|
+
'STRADDLE joint cannot enter under_test without straddle { discriminating_signal, kill_criteria } (STRADDLE-NEEDS-SIGNAL, KILL-CRITERIA-FIRST)',
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
// EXT resolutions MUST carry the ruled result package. Since only the
|
|
200
|
+
// Answerer slice can produce evidence packages, this is also what makes
|
|
201
|
+
// "no EXT resolution can occur before the Answerer exists" structural.
|
|
202
|
+
if (joint.resolve_by === 'EXT' && (to === 'resolved' || to === 'inconclusive')) {
|
|
203
|
+
const extResult = input?.resolution?.ext_result;
|
|
204
|
+
if (!extResult) {
|
|
205
|
+
violations.push(
|
|
206
|
+
'an EXT resolution requires the ruled ext_result package { outcome, sources, search_record, found_or_provoked, judgment_not_evidence }',
|
|
207
|
+
);
|
|
208
|
+
} else {
|
|
209
|
+
if (['FOUND', 'CONTRARY'].includes(extResult.outcome)) {
|
|
210
|
+
const sources = Array.isArray(extResult.sources) ? extResult.sources : [];
|
|
211
|
+
if (sources.length === 0) {
|
|
212
|
+
violations.push(`ext_result ${extResult.outcome} requires at least one source address`);
|
|
213
|
+
}
|
|
214
|
+
for (const source of sources) {
|
|
215
|
+
if (!String(source).startsWith('records/evidence/')) {
|
|
216
|
+
violations.push(`ext_result source "${source}" must address an immutable records/evidence/ package`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (joint.ext?.judgment_dispatch === true && extResult.judgment_dispatch !== true) {
|
|
221
|
+
violations.push('the judgment-dispatch stamp propagates permanently: ext_result.judgment_dispatch must be true on a BAR-OR-JUDGMENT joint');
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
const extOutcome = input?.resolution?.ext_result?.outcome;
|
|
226
|
+
if (extOutcome === 'SILENT' && to !== 'inconclusive') {
|
|
227
|
+
violations.push('a SILENT external-signal result may only yield the joint outcome inconclusive');
|
|
228
|
+
}
|
|
229
|
+
if (violations.length > 0) refuse('JUDGMENT_METHOD_GATE', violations);
|
|
230
|
+
}
|