@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.
Files changed (138) hide show
  1. package/.claude/skills/compose/SKILL.md +42 -88
  2. package/bin/compose.js +288 -0
  3. package/bin/git-hooks/pre-push.template +29 -0
  4. package/bin/judgment-import.js +572 -0
  5. package/contracts/feature-json.schema.json +5 -0
  6. package/contracts/judgment-record.schema.json +821 -0
  7. package/dist/assets/App-PkZzHeMj.js +894 -0
  8. package/dist/assets/_baseUniq-Bo837sRJ.js +1 -0
  9. package/dist/assets/arc-BafGpyqE.js +1 -0
  10. package/dist/assets/architectureDiagram-Q4EWVU46-BOBfUsqL.js +36 -0
  11. package/dist/assets/blockDiagram-DXYQGD6D-Dwodev1a.js +132 -0
  12. package/dist/assets/{browser-BSM23If2.js → browser-1ntj1-x_.js} +6 -6
  13. package/dist/assets/{c4Diagram-LMCZKHZV-CUmTCrMW.js → c4Diagram-AHTNJAMY-CU_bhYag.js} +1 -1
  14. package/dist/assets/channel-qVK_qn4E.js +1 -0
  15. package/dist/assets/{chunk-JWPE2WC7-DQFs7gjy.js → chunk-4BX2VUAB-p8WsDwnO.js} +1 -1
  16. package/dist/assets/chunk-4TB4RGXK-B8h7-eR0.js +206 -0
  17. package/dist/assets/{chunk-XXDRQBXY-CSdt06Hp.js → chunk-55IACEB6-DxeEr98s.js} +1 -1
  18. package/dist/assets/{chunk-VR4S4FIN-Dvhgnm7E.js → chunk-EDXVE4YY-BYt8F151.js} +1 -1
  19. package/dist/assets/{chunk-5VM5RSS4-Qw-L7OoJ.js → chunk-FMBD7UC4-DGSOVeie.js} +1 -1
  20. package/dist/assets/chunk-OYMX7WX6-B-QdgYR2.js +231 -0
  21. package/dist/assets/{chunk-2Q5K7J3B-Cyvi1AFH.js → chunk-QZHKN3VN-Du5UAZLs.js} +1 -1
  22. package/dist/assets/{chunk-32BRIVSS-B1y5FpOd.js → chunk-YZCP3GAM-C8JbNBSk.js} +1 -1
  23. package/dist/assets/classDiagram-6PBFFD2Q-B8UcfC1q.js +1 -0
  24. package/dist/assets/classDiagram-v2-HSJHXN6E-B8UcfC1q.js +1 -0
  25. package/dist/assets/clone-Pu3RyLUh.js +1 -0
  26. package/dist/assets/{cose-bilkent-JH36ORCC-G8l05tXy.js → cose-bilkent-S5V4N54A-O1ESaqge.js} +1 -1
  27. package/dist/assets/dagre-KV5264BT-CPTmFPHw.js +4 -0
  28. package/dist/assets/diagram-5BDNPKRD-B3PNrWs5.js +10 -0
  29. package/dist/assets/diagram-G4DWMVQ6-Cscfr6vc.js +24 -0
  30. package/dist/assets/diagram-MMDJMWI5-CSfqZ-TM.js +43 -0
  31. package/dist/assets/diagram-TYMM5635-Cg4aYS7W.js +24 -0
  32. package/dist/assets/erDiagram-SMLLAGMA-_ZqwG5pl.js +85 -0
  33. package/dist/assets/flowDiagram-DWJPFMVM-C83boxFT.js +162 -0
  34. package/dist/assets/ganttDiagram-T4ZO3ILL-CWnIjuEi.js +292 -0
  35. package/dist/assets/gitGraphDiagram-UUTBAWPF-DrMdxZfH.js +106 -0
  36. package/dist/assets/graph-Bi99_6Yf.js +331 -0
  37. package/dist/assets/graph-RE4I7Ty7.js +1 -0
  38. package/dist/assets/index-Rm2RE-c0.js +123 -0
  39. package/dist/assets/infoDiagram-42DDH7IO-BLmP4Epr.js +2 -0
  40. package/dist/assets/{ishikawaDiagram-FXEZZL3T-Hm4zSDlM.js → ishikawaDiagram-UXIWVN3A-yuWWshKN.js} +5 -5
  41. package/dist/assets/{journeyDiagram-5HDEW3XC-CIqcTFuO.js → journeyDiagram-VCZTEJTY-BOfhaJov.js} +1 -1
  42. package/dist/assets/{kanban-definition-HUTT4EX6-CB9jCXj_.js → kanban-definition-6JOO6SKY-Bbolde15.js} +7 -7
  43. package/dist/assets/katex-DkKDou_j.js +257 -0
  44. package/dist/assets/layout-BSf33zm8.js +1 -0
  45. package/dist/assets/{linear-CsXqv70E.js → linear-AvSTWMqx.js} +1 -1
  46. package/dist/assets/min-QBM8H4xN.js +1 -0
  47. package/dist/assets/{mindmap-definition-LN4V7U3C-CpqH5F_P.js → mindmap-definition-QFDTVHPH-BuvgtqIc.js} +7 -7
  48. package/dist/assets/{mobile-CaoXUwAr.js → mobile-BnXEOE3U.js} +2 -2
  49. package/dist/assets/pieDiagram-DEJITSTG-DIzF16vh.js +30 -0
  50. package/dist/assets/quadrantDiagram-34T5L4WZ-D-mbUIjS.js +7 -0
  51. package/dist/assets/{requirementDiagram-TGXJPOKE-D1lG1P7h.js → requirementDiagram-MS252O5E-CEs4kCLd.js} +3 -3
  52. package/dist/assets/sankeyDiagram-XADWPNL6-DFsnCr9n.js +10 -0
  53. package/dist/assets/sequenceDiagram-FGHM5R23-BEJYdTjQ.js +157 -0
  54. package/dist/assets/stateDiagram-FHFEXIEX-BBXs57uY.js +1 -0
  55. package/dist/assets/stateDiagram-v2-QKLJ7IA2-BqKuX4rj.js +1 -0
  56. package/dist/assets/{timeline-definition-FHXFAJF6-CzqqUDvt.js → timeline-definition-GMOUNBTQ-BGvLoVAY.js} +3 -3
  57. package/dist/assets/vennDiagram-DHZGUBPP-9LaBTMe0.js +34 -0
  58. package/dist/assets/wardley-RL74JXVD-P4MEqMTP.js +162 -0
  59. package/dist/assets/wardleyDiagram-NUSXRM2D-o-tmxnlC.js +20 -0
  60. package/dist/assets/xychartDiagram-5P7HB3ND-Dpn7V6qk.js +7 -0
  61. package/dist/index.html +2 -2
  62. package/lib/bug-escalation.js +30 -4
  63. package/lib/build.js +777 -52
  64. package/lib/canon-guard.js +223 -0
  65. package/lib/canon-registry.js +187 -0
  66. package/lib/codex-preflight.js +26 -4
  67. package/lib/dispatch-ledger.js +301 -0
  68. package/lib/dispatch-metrics.js +236 -0
  69. package/lib/experiment-judge.js +6 -1
  70. package/lib/feature-writer.js +9 -0
  71. package/lib/gsd.js +11 -2
  72. package/lib/hooks-status.js +32 -3
  73. package/lib/idempotency.js +33 -8
  74. package/lib/judgment/schema.js +23 -0
  75. package/lib/judgment/store/index.js +207 -0
  76. package/lib/judgment/store/records.js +405 -0
  77. package/lib/judgment-attest.js +259 -0
  78. package/lib/judgment-gen.js +691 -0
  79. package/lib/judgment-verify.js +153 -0
  80. package/lib/judgment-write-guard.js +230 -0
  81. package/lib/judgment-writer.js +3403 -0
  82. package/lib/lane-gate.js +2 -0
  83. package/lib/lifecycle-modes.js +42 -0
  84. package/lib/local-claude-connector.js +199 -54
  85. package/lib/mcp-enforcement.js +21 -35
  86. package/lib/result-normalizer.js +93 -15
  87. package/lib/review-normalize.js +4 -0
  88. package/lib/stratum-mcp-client.js +131 -6
  89. package/package.json +2 -2
  90. package/server/compose-mcp-tools.js +49 -1
  91. package/server/compose-mcp.js +220 -0
  92. package/server/mcp-tool-policy.js +1 -0
  93. package/dist/assets/App-BG92y_CY.js +0 -896
  94. package/dist/assets/abnfDiagram-VRR7QNED-Bip6cMjP.js +0 -1
  95. package/dist/assets/arc-CyMTM07c.js +0 -1
  96. package/dist/assets/architectureDiagram-ZJ3FMSHR-BukV0CJw.js +0 -36
  97. package/dist/assets/blockDiagram-677ZJIJ3-Bjb_MNB4.js +0 -132
  98. package/dist/assets/channel-BzkmtaLy.js +0 -1
  99. package/dist/assets/chunk-EX3LRPZG-G_MZEji_.js +0 -231
  100. package/dist/assets/chunk-MOJQB5TN-CQZQYMcz.js +0 -88
  101. package/dist/assets/chunk-RYQCIY6F-CmAda4sZ.js +0 -1
  102. package/dist/assets/chunk-V7JOEXUC-CbjBz-Ek.js +0 -206
  103. package/dist/assets/classDiagram-OUVF2IWQ-BIvLcbex.js +0 -1
  104. package/dist/assets/classDiagram-v2-EOCWNBFH-BIvLcbex.js +0 -1
  105. package/dist/assets/cynefin-VYW2F7L2-C3n1PNJC.js +0 -178
  106. package/dist/assets/cynefinDiagram-TSTJHNR4-C8N_CvKJ.js +0 -62
  107. package/dist/assets/dagre-VKFMJZFB-B2pkwDhN.js +0 -4
  108. package/dist/assets/diagram-FQU43EPY-DPHD_t-1.js +0 -3
  109. package/dist/assets/diagram-G47NLZAW-DGmHIZRr.js +0 -24
  110. package/dist/assets/diagram-NH7WQ7WH-BlLWwKVt.js +0 -24
  111. package/dist/assets/diagram-OA4YK3LP-DEv704Re.js +0 -30
  112. package/dist/assets/diagram-WEI45ONY-Bj6PGUCe.js +0 -41
  113. package/dist/assets/ebnfDiagram-CCIWWBDH-Ch-icksT.js +0 -1
  114. package/dist/assets/erDiagram-Q63AITRT-CI3lwEFA.js +0 -85
  115. package/dist/assets/flowDiagram-23GEKE2U-DcUANpAz.js +0 -156
  116. package/dist/assets/ganttDiagram-NO4QXBWP-Cy0Ie8KS.js +0 -292
  117. package/dist/assets/gitGraphDiagram-IHSO6WYX-BswI52az.js +0 -106
  118. package/dist/assets/graph-BXPQrYYB.js +0 -331
  119. package/dist/assets/graph-C9eacEi8.js +0 -1
  120. package/dist/assets/index-B1H7-G2y.js +0 -119
  121. package/dist/assets/infoDiagram-FWYZ7A6U-BBunqvr-.js +0 -2
  122. package/dist/assets/katex-C5jXJg4s.js +0 -257
  123. package/dist/assets/layout-DEXfKzaS.js +0 -1
  124. package/dist/assets/map-Czzmt4hB.js +0 -1
  125. package/dist/assets/pegDiagram-2B236MQR-HpQyrDcm.js +0 -1
  126. package/dist/assets/pieDiagram-ENE6RG2P-xkcpHwnY.js +0 -39
  127. package/dist/assets/quadrantDiagram-ABIIQ3AL-kebZIOmu.js +0 -7
  128. package/dist/assets/railroadDiagram-RFXS5EU6-DNvGwWm9.js +0 -1
  129. package/dist/assets/sankeyDiagram-HTMAVEWB-DOEEcn52.js +0 -40
  130. package/dist/assets/sequenceDiagram-DBY2YBRQ-DynIVCk_.js +0 -162
  131. package/dist/assets/sizeCapture-X5ZJPWSS-DJXFjUUL.js +0 -1
  132. package/dist/assets/stateDiagram-2N3HPSRC-OZ7trFh5.js +0 -1
  133. package/dist/assets/stateDiagram-v2-6OUMAXLB-GcPRHAJT.js +0 -1
  134. package/dist/assets/swimlanes-5IMT3BWC-DULYAFmB.js +0 -2
  135. package/dist/assets/swimlanesDiagram-G3AALYLV-BB0gCy01.js +0 -8
  136. package/dist/assets/vennDiagram-L72KCM5P-CdKmtcFL.js +0 -34
  137. package/dist/assets/wardleyDiagram-EHGQE667-DknGwjCA.js +0 -78
  138. 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
+ }