@smartmemory/compose 0.3.6-beta → 0.3.8
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/.compose-deps.json +1 -13
- package/README.md +72 -5
- package/bin/compose.js +754 -347
- package/bin/git-hooks/pre-push.template +29 -0
- package/bin/judgment-import.js +7 -0
- package/bin/judgment-migrate.js +387 -0
- package/contracts/comp-obs-contract.schema.json +9 -3
- package/contracts/feature-json.schema.json +5 -0
- package/contracts/fluid-record.schema.json +209 -0
- package/contracts/judgment-record.schema.json +425 -4
- package/contracts/lifecycle-backfill.schema.json +322 -0
- package/dist/assets/App-Z4MU-H_F.js +916 -0
- package/dist/assets/_baseUniq-ClWoCPFl.js +1 -0
- package/dist/assets/arc-DY26UIVo.js +1 -0
- package/dist/assets/architectureDiagram-Q4EWVU46-6Ggq4DqJ.js +36 -0
- package/dist/assets/blockDiagram-DXYQGD6D-CH3Ked0l.js +132 -0
- package/dist/assets/{browser-BSM23If2.js → browser-BWkrenen.js} +6 -6
- package/dist/assets/{c4Diagram-LMCZKHZV-DZf45Fbz.js → c4Diagram-AHTNJAMY-Bk8dYilu.js} +1 -1
- package/dist/assets/channel-SnZzzh7k.js +1 -0
- package/dist/assets/{chunk-JWPE2WC7-_7ujgd_Q.js → chunk-4BX2VUAB-BMR0XaAQ.js} +1 -1
- package/dist/assets/chunk-4TB4RGXK-JytR14a9.js +206 -0
- package/dist/assets/{chunk-XXDRQBXY-DfdVhbmA.js → chunk-55IACEB6-B4Q97BCP.js} +1 -1
- package/dist/assets/{chunk-VR4S4FIN-Dt9NZ67m.js → chunk-EDXVE4YY-R_qarkSf.js} +1 -1
- package/dist/assets/{chunk-5VM5RSS4-BY4_PV5H.js → chunk-FMBD7UC4-C9s7KR9m.js} +1 -1
- package/dist/assets/chunk-OYMX7WX6-BySQzVxc.js +231 -0
- package/dist/assets/{chunk-2Q5K7J3B-Dn1spZYu.js → chunk-QZHKN3VN-DdpSYZsW.js} +1 -1
- package/dist/assets/{chunk-32BRIVSS-pURGrJDk.js → chunk-YZCP3GAM-iE_tzriw.js} +1 -1
- package/dist/assets/classDiagram-6PBFFD2Q-CBu92dSH.js +1 -0
- package/dist/assets/classDiagram-v2-HSJHXN6E-CBu92dSH.js +1 -0
- package/dist/assets/clone-DgklGjHm.js +1 -0
- package/dist/assets/{cose-bilkent-JH36ORCC-BieYif4o.js → cose-bilkent-S5V4N54A-BdlU6ZX_.js} +1 -1
- package/dist/assets/dagre-KV5264BT-Cp3F5KTn.js +4 -0
- package/dist/assets/diagram-5BDNPKRD-DiR6_2q_.js +10 -0
- package/dist/assets/diagram-G4DWMVQ6-w0i-p5HX.js +24 -0
- package/dist/assets/diagram-MMDJMWI5-tIHhwUv3.js +43 -0
- package/dist/assets/diagram-TYMM5635-BAeY3B19.js +24 -0
- package/dist/assets/erDiagram-SMLLAGMA-Ckx_Knko.js +85 -0
- package/dist/assets/flowDiagram-DWJPFMVM-DeoNka6J.js +162 -0
- package/dist/assets/ganttDiagram-T4ZO3ILL-BmGnFbEg.js +292 -0
- package/dist/assets/gitGraphDiagram-UUTBAWPF-Dk48IHsx.js +106 -0
- package/dist/assets/graph-BNzKGvoy.js +1 -0
- package/dist/assets/graph-CI_1htl0.js +331 -0
- package/dist/assets/index-BEfrNBp8.js +123 -0
- package/dist/assets/index-yyrA5OZd.css +1 -0
- package/dist/assets/infoDiagram-42DDH7IO-BRf827i0.js +2 -0
- package/dist/assets/{ishikawaDiagram-FXEZZL3T-CzEB9fQS.js → ishikawaDiagram-UXIWVN3A-0kCZaeCM.js} +5 -5
- package/dist/assets/{journeyDiagram-5HDEW3XC-Bz8TCdz2.js → journeyDiagram-VCZTEJTY-rvU7ayRt.js} +1 -1
- package/dist/assets/{kanban-definition-HUTT4EX6-tozrMoV_.js → kanban-definition-6JOO6SKY-DpQwX1C5.js} +7 -7
- package/dist/assets/katex-DkKDou_j.js +257 -0
- package/dist/assets/layout-BI8cXFPI.js +1 -0
- package/dist/assets/{linear-Ck7gpa5N.js → linear-a0glcDiw.js} +1 -1
- package/dist/assets/min-vPHfnXcC.js +1 -0
- package/dist/assets/{mindmap-definition-LN4V7U3C-DTcHO0DJ.js → mindmap-definition-QFDTVHPH-D14eF-7C.js} +7 -7
- package/dist/assets/mobile-B7m9EO9D.js +17 -0
- package/dist/assets/pieDiagram-DEJITSTG-Cno-gETh.js +30 -0
- package/dist/assets/quadrantDiagram-34T5L4WZ-BUQM1Hfm.js +7 -0
- package/dist/assets/{requirementDiagram-TGXJPOKE-bnI2zJeT.js → requirementDiagram-MS252O5E-pOXlN2-q.js} +3 -3
- package/dist/assets/sankeyDiagram-XADWPNL6-Crynd3_b.js +10 -0
- package/dist/assets/sequenceDiagram-FGHM5R23-D9fZdCM8.js +157 -0
- package/dist/assets/stateDiagram-FHFEXIEX-CW9qVec8.js +1 -0
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-DkVLzHbY.js +1 -0
- package/dist/assets/{timeline-definition-FHXFAJF6-D267GQFF.js → timeline-definition-GMOUNBTQ-BcHzhm_8.js} +3 -3
- package/dist/assets/vennDiagram-DHZGUBPP-BfytJcWk.js +34 -0
- package/dist/assets/wardley-RL74JXVD-DLj-IjyB.js +162 -0
- package/dist/assets/wardleyDiagram-NUSXRM2D-Ds0Ue68c.js +20 -0
- package/dist/assets/xychartDiagram-5P7HB3ND-vjWDXFL6.js +7 -0
- package/dist/index.html +3 -3
- package/lib/agent-string.js +7 -5
- package/lib/append-integrity.js +81 -0
- package/lib/backfill-evidence.js +109 -0
- package/lib/bug-escalation.js +39 -4
- package/lib/build-stream-schema.js +3 -1
- package/lib/build-stream-writer.js +25 -0
- package/lib/build.js +1624 -195
- package/lib/canon-guard.js +245 -0
- package/lib/canon-override.js +196 -0
- package/lib/canon-registry.js +291 -0
- package/lib/cli-commands.js +144 -0
- package/lib/codex-preflight.js +50 -15
- package/lib/colleague/context.js +215 -0
- package/lib/colleague/writeback.js +95 -0
- package/lib/completion-gate.js +1421 -0
- package/lib/completion-writer.js +47 -47
- package/lib/consumer-fanout.js +105 -11
- package/lib/coverage-gate.js +200 -0
- package/lib/dir-lock.js +170 -0
- package/lib/dispatch-ledger.js +301 -0
- package/lib/dispatch-metrics.js +236 -0
- package/lib/experiment-judge.js +6 -1
- package/lib/feature-json.js +1 -1
- package/lib/feature-reconciler.js +8 -0
- package/lib/feature-validator.js +64 -1
- package/lib/feature-writer.js +66 -2
- package/lib/fluid/factory.js +167 -0
- package/lib/fluid/ideabox-dates.js +73 -0
- package/lib/fluid/ideabox-migrate.js +154 -0
- package/lib/fluid/ideabox-ops.js +585 -0
- package/lib/fluid/ideabox-view.js +146 -0
- package/lib/fluid/import-ideabox.js +186 -0
- package/lib/fluid/local-provider.js +606 -0
- package/lib/fluid/provider.js +684 -0
- package/lib/fluid/record-shape.js +214 -0
- package/lib/fluid/record-store.js +328 -0
- package/lib/fluid/render-ideabox.js +261 -0
- package/lib/fluid/schema.js +40 -0
- package/lib/fluid/smartmemory-provider.js +1695 -0
- package/lib/gsd.js +63 -14
- package/lib/guard-cli.js +175 -0
- package/lib/guard-custody.js +141 -0
- package/lib/guard-descriptors.js +530 -0
- package/lib/guard-enrol.js +254 -0
- package/lib/health-score.js +1 -1
- package/lib/hooks-status.js +32 -3
- package/lib/ideabox-cli.js +315 -0
- package/lib/ideabox.js +121 -21
- package/lib/judgment/store/index.js +166 -0
- package/lib/judgment/store/records.js +184 -25
- package/lib/judgment/trace.js +380 -0
- package/lib/judgment-attest.js +259 -0
- package/lib/judgment-decision-write.js +277 -0
- package/lib/judgment-decisions.js +466 -0
- package/lib/judgment-gen.js +375 -22
- package/lib/judgment-verify.js +153 -0
- package/lib/judgment-writer.js +2842 -262
- package/lib/lane-gate.js +2 -0
- package/lib/lifecycle-modes.js +4 -4
- package/lib/lineage.js +400 -0
- package/lib/local-claude-connector.js +250 -54
- package/lib/maya-client.js +302 -0
- package/lib/maya-config.js +53 -0
- package/lib/maya-identity.js +283 -0
- package/lib/mcp-enforcement.js +21 -35
- package/lib/migrate-anon.js +5 -0
- package/lib/migrate-roadmap.js +15 -0
- package/lib/new.js +13 -1
- package/lib/pipeline-compat.js +104 -0
- package/lib/policy-catalog.js +295 -0
- package/lib/policy-check.js +0 -0
- package/lib/process-termination.js +98 -0
- package/lib/resolve-workspace.js +5 -1
- package/lib/result-normalizer.js +428 -153
- package/lib/review-normalize.js +4 -0
- package/lib/roadmap-errors.js +65 -0
- package/lib/roadmap-preservers.js +24 -4
- package/lib/roadmap-residue.js +299 -0
- package/lib/smartmemory-client.js +614 -78
- package/lib/smartmemory-config.js +54 -0
- package/lib/smartmemory-ingest.js +19 -2
- package/lib/step-prompt.js +7 -6
- package/lib/stratum-engine.js +53 -4
- package/lib/stratum-mcp-client.js +391 -31
- package/lib/test-bootstrap.js +31 -0
- package/lib/tool-inventory.js +122 -0
- package/lib/version-check.js +91 -19
- package/lib/vision-writer.js +88 -1
- package/package.json +7 -6
- package/pipelines/bug-fix.stratum.yaml +205 -211
- package/pipelines/build-quick.profiles.json +12 -0
- package/pipelines/build-quick.stratum.yaml +263 -350
- package/pipelines/content.stratum.yaml +81 -77
- package/pipelines/coverage-sweep.stratum.yaml +49 -30
- package/pipelines/plan.stratum.yaml +76 -86
- package/pipelines/refactor.stratum.yaml +125 -125
- package/pipelines/research.stratum.yaml +56 -58
- package/pipelines/review-fix.profiles.json +6 -0
- package/pipelines/review-fix.stratum.yaml +110 -83
- package/presets/team-feature.profiles.json +6 -0
- package/presets/team-feature.stratum.yaml +93 -66
- package/presets/team-research.profiles.json +6 -0
- package/presets/team-research.stratum.yaml +89 -80
- package/presets/team-review.profiles.json +8 -0
- package/presets/team-review.stratum.yaml +98 -80
- package/scripts/cost-census.mjs +70 -0
- package/scripts/guard-sign/compose-guard-sign.sh +62 -0
- package/server/agent-health.js +22 -0
- package/server/agent-hooks.js +14 -1
- package/server/agent-server.js +5 -248
- package/server/agent-spawn.js +3 -4
- package/server/agent-workspace.js +294 -0
- package/server/build-routes.js +6 -5
- package/server/build-stream-bridge.js +53 -0
- package/server/cc-session-watcher.js +4 -1
- package/server/coalescing-buffer.js +7 -1
- package/server/completion-projection.js +228 -0
- package/server/compose-mcp-tools.js +124 -24
- package/server/compose-mcp.js +91 -790
- package/server/decision-event-emit.js +41 -2
- package/server/decision-event-id.js +17 -0
- package/server/decision-events-snapshot.js +3 -0
- package/server/design-routes.js +14 -8
- package/server/feature-scan.js +76 -2
- package/server/file-watcher.js +170 -21
- package/server/ideabox-routes.js +166 -224
- package/server/index.js +70 -100
- package/server/lifecycle-guard.js +240 -10
- package/server/lifecycle-phase-history.js +276 -0
- package/server/maya-routes.js +507 -0
- package/server/mcp-tool-defs.js +940 -0
- package/server/mcp-tool-policy.js +35 -3
- package/server/model-tiers.js +22 -5
- package/server/pipeline-routes.js +21 -11
- package/server/project-root.js +58 -19
- package/server/remote-utils.js +3 -1
- package/server/schema-validator.js +7 -1
- package/server/session-manager.js +5 -6
- package/server/session-routes.js +3 -1
- package/server/stratum-client.js +57 -10
- package/server/stratum-sync.js +6 -3
- package/server/summarizer.js +3 -4
- package/server/supervisor.js +0 -1
- package/server/vision-routes.js +208 -98
- package/server/vision-server.js +86 -23
- package/server/vision-store.js +60 -6
- package/server/vision-utils.js +3 -4
- package/server/workspace-activity.js +18 -0
- package/server/workspace-middleware.js +2 -2
- package/server/workspace-runtime.js +243 -0
- package/server/worktree-gc.js +1 -0
- package/dist/assets/App-BG3ngu8H.js +0 -896
- package/dist/assets/abnfDiagram-VRR7QNED-CjB_sD3D.js +0 -1
- package/dist/assets/arc-_v4hR_uD.js +0 -1
- package/dist/assets/architectureDiagram-ZJ3FMSHR-DreJmzXQ.js +0 -36
- package/dist/assets/blockDiagram-677ZJIJ3-BG9-c0O1.js +0 -132
- package/dist/assets/channel-B3U5wFAT.js +0 -1
- package/dist/assets/chunk-EX3LRPZG-DdELs1qP.js +0 -231
- package/dist/assets/chunk-MOJQB5TN-D-ky35G-.js +0 -88
- package/dist/assets/chunk-RYQCIY6F-Dag_kVlO.js +0 -1
- package/dist/assets/chunk-V7JOEXUC-BtewURat.js +0 -206
- package/dist/assets/classDiagram-OUVF2IWQ-B6fCN-ht.js +0 -1
- package/dist/assets/classDiagram-v2-EOCWNBFH-B6fCN-ht.js +0 -1
- package/dist/assets/cynefin-VYW2F7L2-CT2BA6KE.js +0 -178
- package/dist/assets/cynefinDiagram-TSTJHNR4-Bh6exbyg.js +0 -62
- package/dist/assets/dagre-VKFMJZFB-aXMLSmQL.js +0 -4
- package/dist/assets/diagram-FQU43EPY-Dr7JAOuQ.js +0 -3
- package/dist/assets/diagram-G47NLZAW-DUvA3FQK.js +0 -24
- package/dist/assets/diagram-NH7WQ7WH-BQUARqcu.js +0 -24
- package/dist/assets/diagram-OA4YK3LP-dDUc1zHi.js +0 -30
- package/dist/assets/diagram-WEI45ONY-B2h5Qlb1.js +0 -41
- package/dist/assets/ebnfDiagram-CCIWWBDH-DThRGupB.js +0 -1
- package/dist/assets/erDiagram-Q63AITRT-BUCsprO2.js +0 -85
- package/dist/assets/flowDiagram-23GEKE2U-DXtNNi6r.js +0 -156
- package/dist/assets/ganttDiagram-NO4QXBWP-D4zbBHh_.js +0 -292
- package/dist/assets/gitGraphDiagram-IHSO6WYX-DpoQws0W.js +0 -106
- package/dist/assets/graph-BXPQrYYB.js +0 -331
- package/dist/assets/graph-C9eacEi8.js +0 -1
- package/dist/assets/index-3ZH5eMcZ.js +0 -119
- package/dist/assets/index-LIwREYgH.css +0 -1
- package/dist/assets/infoDiagram-FWYZ7A6U-Bbas2GAo.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/mobile-CaoXUwAr.js +0 -17
- package/dist/assets/pegDiagram-2B236MQR-CHiINrNy.js +0 -1
- package/dist/assets/pieDiagram-ENE6RG2P-CfS4YFlR.js +0 -39
- package/dist/assets/quadrantDiagram-ABIIQ3AL-CadesS9w.js +0 -7
- package/dist/assets/railroadDiagram-RFXS5EU6-CgWEspBN.js +0 -1
- package/dist/assets/sankeyDiagram-HTMAVEWB-YWKFgOGw.js +0 -40
- package/dist/assets/sequenceDiagram-DBY2YBRQ-BvkNOyF9.js +0 -162
- package/dist/assets/sizeCapture-X5ZJPWSS-DlFPA2yO.js +0 -1
- package/dist/assets/stateDiagram-2N3HPSRC-h8NIx0kQ.js +0 -1
- package/dist/assets/stateDiagram-v2-6OUMAXLB-DjPgZtJ9.js +0 -1
- package/dist/assets/swimlanes-5IMT3BWC-CT5n22kG.js +0 -2
- package/dist/assets/swimlanesDiagram-G3AALYLV-Dn318Bhq.js +0 -8
- package/dist/assets/vennDiagram-L72KCM5P-Dj-wWLYG.js +0 -34
- package/dist/assets/wardleyDiagram-EHGQE667-BxCeYxkG.js +0 -78
- package/dist/assets/xychartDiagram-FW5EYKEG-DMFqWn7z.js +0 -7
- package/lib/staleness.js +0 -87
- package/server/ideabox-cache.js +0 -77
package/lib/judgment-gen.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* lib/judgment-gen.js — judgment projections (S04).
|
|
3
3
|
*
|
|
4
|
-
* records → REGISTER.md, LEDGER.md, OBJECTIVE.md,
|
|
5
|
-
* the OKF bundle root index.md.
|
|
4
|
+
* records → REGISTER.md, LEDGER.md, OBJECTIVE.md, SITUATION.md,
|
|
5
|
+
* people/<slug>.md, positions/<slug>.md, and the OKF bundle root index.md.
|
|
6
|
+
* Projections are PURE OUTPUT: generation
|
|
6
7
|
* reads only records — never an existing projection — so any hand-edit is
|
|
7
8
|
* overwritten by the next regeneration and there are no preserved sections
|
|
8
9
|
* (curated prose is `note` records rendered at their anchors).
|
|
@@ -22,7 +23,12 @@ import {
|
|
|
22
23
|
readFileSync, writeFileSync, renameSync, unlinkSync, mkdirSync, existsSync, readdirSync,
|
|
23
24
|
} from 'node:fs';
|
|
24
25
|
import { join } from 'node:path';
|
|
25
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
createJudgmentStore,
|
|
28
|
+
effectiveStore,
|
|
29
|
+
goalCutoverComplete,
|
|
30
|
+
} from './judgment/store/index.js';
|
|
31
|
+
import { buildSupersessionIndex } from './judgment/trace.js';
|
|
26
32
|
|
|
27
33
|
function atomicWrite(path, content) {
|
|
28
34
|
const tmp = `${path}.tmp.${process.pid}`;
|
|
@@ -50,17 +56,26 @@ function smartmemoryTeamId(cwd) {
|
|
|
50
56
|
* Load everything projection-relevant from the store into a plain snapshot,
|
|
51
57
|
* so the generator core stays pure.
|
|
52
58
|
*/
|
|
53
|
-
function loadSnapshot(cwd) {
|
|
54
|
-
const
|
|
59
|
+
export function loadSnapshot(cwd) {
|
|
60
|
+
const rawStore = createJudgmentStore(cwd);
|
|
61
|
+
const store = effectiveStore(rawStore);
|
|
62
|
+
// Prebuilt index: derivePositionStatus otherwise rescans every other slug's
|
|
63
|
+
// chain per call, making this whole-store pass O(n^2) (COMP-JUDGMENT-PRECEDENT).
|
|
64
|
+
const supersession = buildSupersessionIndex(store);
|
|
55
65
|
const positions = store.listPositionSlugs().map((slug) => ({
|
|
56
66
|
slug,
|
|
57
67
|
chain: store.readPositionChain(slug),
|
|
58
|
-
status: store.derivePositionStatus(slug),
|
|
68
|
+
status: store.derivePositionStatus(slug, supersession),
|
|
59
69
|
}));
|
|
60
70
|
return {
|
|
61
71
|
positions,
|
|
62
72
|
joints: store.listJoints(),
|
|
63
73
|
ledger: store.readLedgerEvents(),
|
|
74
|
+
people: store.listPeople(),
|
|
75
|
+
situationEntities: store.listSituationEntities(),
|
|
76
|
+
goalChain: store.readGoalChain(),
|
|
77
|
+
goalState: store.readGoalState(),
|
|
78
|
+
goalCutoverComplete: goalCutoverComplete(store),
|
|
64
79
|
teamId: smartmemoryTeamId(cwd),
|
|
65
80
|
};
|
|
66
81
|
}
|
|
@@ -75,11 +90,22 @@ function renderNotes(notes) {
|
|
|
75
90
|
return notes.map((n) => `> **${n.title}** — ${n.body ?? ''}`.trimEnd()).join('\n\n');
|
|
76
91
|
}
|
|
77
92
|
|
|
93
|
+
// Plain YAML scalars that need no quoting: no leading/trailing space, none
|
|
94
|
+
// of the indicator/structure characters that would change the parse. Risky
|
|
95
|
+
// titles (e.g. "Jane: CEO") are emitted JSON-quoted, which is valid YAML
|
|
96
|
+
// double-quote style.
|
|
97
|
+
const YAML_PLAIN_SAFE_RE = /^[A-Za-z0-9](?:[A-Za-z0-9 _./()-]*[A-Za-z0-9_./()-])?$/;
|
|
98
|
+
|
|
99
|
+
function yamlScalar(value) {
|
|
100
|
+
const s = String(value);
|
|
101
|
+
return YAML_PLAIN_SAFE_RE.test(s) ? s : JSON.stringify(s);
|
|
102
|
+
}
|
|
103
|
+
|
|
78
104
|
function okfFrontmatter({ type, title, timestamp, resource }) {
|
|
79
105
|
const lines = ['---'];
|
|
80
106
|
if (type) lines.push(`type: ${type}`);
|
|
81
107
|
else lines.push('okf_version: "0.1"');
|
|
82
|
-
lines.push(`title: ${title}`);
|
|
108
|
+
lines.push(`title: ${yamlScalar(title)}`);
|
|
83
109
|
if (timestamp) lines.push(`timestamp: "${timestamp}"`);
|
|
84
110
|
if (resource) lines.push(`resource: "${resource}"`);
|
|
85
111
|
lines.push('smartmemory:');
|
|
@@ -94,6 +120,194 @@ function buildResource(teamId, providerId) {
|
|
|
94
120
|
return `smartmemory://${encodeURIComponent(teamId)}/${encodeURIComponent(providerId)}`;
|
|
95
121
|
}
|
|
96
122
|
|
|
123
|
+
function auditValue(value) {
|
|
124
|
+
const encoded = JSON.stringify(value);
|
|
125
|
+
return encoded === undefined ? String(value) : encoded;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function renderTraceLines(trace = [], { prefix = ' - ' } = {}) {
|
|
129
|
+
return trace.map((entry) => {
|
|
130
|
+
const prior = Object.entries(entry.prior ?? {})
|
|
131
|
+
.map(([key, value]) => `${key}=${auditValue(value)}`)
|
|
132
|
+
.join(', ');
|
|
133
|
+
return `${prefix}corrected from ${prior} at ${entry.corrected_at ?? 'unknown'}`;
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function renderRecordedAndRemoval(entry, { prefix = ' - ' } = {}) {
|
|
138
|
+
const lines = [];
|
|
139
|
+
lines.push(`${prefix}recorded at ${entry.provenance?.written_at ?? 'unknown'}`);
|
|
140
|
+
if (entry.removed) {
|
|
141
|
+
lines.push(`${prefix}removed at ${entry.removed.at ?? 'unknown'} — ${entry.removed.reason}`);
|
|
142
|
+
}
|
|
143
|
+
return lines;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function renderAuditedFact(fact) {
|
|
147
|
+
const lines = [`- **${fact.id}** ${fact.text}`];
|
|
148
|
+
lines.push(` - channel: \`${fact.channel}\``);
|
|
149
|
+
if (fact.via !== undefined) lines.push(` - via: ${fact.via}`);
|
|
150
|
+
lines.push(` - at: ${fact.at}`);
|
|
151
|
+
lines.push(...renderTraceLines(fact.trace));
|
|
152
|
+
return lines.join('\n');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const PERSON_SECTIONS = ['role', 'life', 'stated', 'revealed'];
|
|
156
|
+
|
|
157
|
+
function sectionTitle(section) {
|
|
158
|
+
return section[0].toUpperCase() + section.slice(1);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function orderedPairLabel(left, right) {
|
|
162
|
+
const suffix = (id) => Number.parseInt(String(id).replace(/^[^0-9]+/, ''), 10);
|
|
163
|
+
const ids = [left, right].sort((a, b) => suffix(a) - suffix(b) || a.localeCompare(b));
|
|
164
|
+
return `${ids[0]} ↔ ${ids[1]}`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function renderPerson(person) {
|
|
168
|
+
const lifecycle = person.facts.some((fact) => fact.channel === 'said') ? 'spoken' : 'stub';
|
|
169
|
+
const parts = [okfFrontmatter({
|
|
170
|
+
type: 'person',
|
|
171
|
+
title: person.display_name,
|
|
172
|
+
timestamp: person.provenance?.written_at,
|
|
173
|
+
})];
|
|
174
|
+
parts.push('');
|
|
175
|
+
parts.push(`# ${person.display_name}`);
|
|
176
|
+
parts.push('');
|
|
177
|
+
parts.push(`**Lifecycle:** ${lifecycle}`);
|
|
178
|
+
parts.push(`**Record:** \`${person.slug}\``);
|
|
179
|
+
|
|
180
|
+
const factsById = new Map(person.facts.map((fact) => [fact.id, fact]));
|
|
181
|
+
const rendered = new Set();
|
|
182
|
+
for (const section of PERSON_SECTIONS) {
|
|
183
|
+
parts.push('');
|
|
184
|
+
parts.push(`## ${sectionTitle(section)}`);
|
|
185
|
+
parts.push('');
|
|
186
|
+
const sectionLines = [];
|
|
187
|
+
for (const fact of person.facts.filter((entry) => entry.section === section)) {
|
|
188
|
+
if (rendered.has(fact.id)) continue;
|
|
189
|
+
const paired = fact.diverges_with ? factsById.get(fact.diverges_with) : null;
|
|
190
|
+
const isStatedRevealedPair = (
|
|
191
|
+
section === 'stated'
|
|
192
|
+
&& paired?.section === 'revealed'
|
|
193
|
+
&& paired.diverges_with === fact.id
|
|
194
|
+
);
|
|
195
|
+
sectionLines.push(renderAuditedFact(fact));
|
|
196
|
+
rendered.add(fact.id);
|
|
197
|
+
if (isStatedRevealedPair) {
|
|
198
|
+
sectionLines.push(renderAuditedFact(paired));
|
|
199
|
+
sectionLines.push(` - divergence pair: ${orderedPairLabel(fact.id, paired.id)}`);
|
|
200
|
+
rendered.add(paired.id);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
parts.push(sectionLines.length > 0 ? sectionLines.join('\n') : 'No facts recorded.');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
parts.push('');
|
|
207
|
+
parts.push('## Edges');
|
|
208
|
+
parts.push('');
|
|
209
|
+
if (person.edges.length === 0) {
|
|
210
|
+
parts.push('None.');
|
|
211
|
+
} else {
|
|
212
|
+
for (const edge of person.edges) {
|
|
213
|
+
parts.push(`- **${edge.id}** \`${edge.removed ? 'removed' : 'active'}\` — ${edge.kind} → [${edge.to}](${edge.to}.md)`);
|
|
214
|
+
parts.push(...renderRecordedAndRemoval(edge));
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
parts.push('');
|
|
219
|
+
parts.push('## Open fields');
|
|
220
|
+
parts.push('');
|
|
221
|
+
if (person.open_fields.length === 0) {
|
|
222
|
+
parts.push('None.');
|
|
223
|
+
} else {
|
|
224
|
+
for (const field of person.open_fields) {
|
|
225
|
+
parts.push(`- **${field.id}** \`${field.status}\` — ${field.name}`);
|
|
226
|
+
parts.push(` - filled_by: ${field.filled_by ?? '—'}`);
|
|
227
|
+
parts.push(` - recorded at ${field.provenance?.written_at ?? 'unknown'}`);
|
|
228
|
+
parts.push(...renderTraceLines(field.trace));
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
parts.push('');
|
|
233
|
+
parts.push('## Load links');
|
|
234
|
+
parts.push('');
|
|
235
|
+
if (person.load_links.length === 0) {
|
|
236
|
+
parts.push('None.');
|
|
237
|
+
} else {
|
|
238
|
+
for (const link of person.load_links) {
|
|
239
|
+
parts.push(`- **${link.id}** \`${link.removed ? 'removed' : 'active'}\` — fact ${link.fact} carries ${link.carries}`);
|
|
240
|
+
parts.push(...renderRecordedAndRemoval(link));
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return parts.join('\n') + '\n';
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function latestWrittenAt(records) {
|
|
247
|
+
return records
|
|
248
|
+
.map((record) => record.provenance?.written_at)
|
|
249
|
+
.filter(Boolean)
|
|
250
|
+
.sort()
|
|
251
|
+
.at(-1);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function renderSituation(snapshot) {
|
|
255
|
+
const entities = [...(snapshot.situationEntities ?? [])].sort((a, b) => (
|
|
256
|
+
a.display_name.localeCompare(b.display_name) || a.slug.localeCompare(b.slug)
|
|
257
|
+
));
|
|
258
|
+
const parts = [okfFrontmatter({
|
|
259
|
+
type: 'situation',
|
|
260
|
+
title: 'Situation',
|
|
261
|
+
timestamp: latestWrittenAt(entities),
|
|
262
|
+
})];
|
|
263
|
+
parts.push('');
|
|
264
|
+
parts.push('# Situation');
|
|
265
|
+
if (entities.length === 0) {
|
|
266
|
+
parts.push('');
|
|
267
|
+
parts.push('No situation entities recorded.');
|
|
268
|
+
}
|
|
269
|
+
for (const entity of entities) {
|
|
270
|
+
parts.push('');
|
|
271
|
+
parts.push(`## ${entity.display_name} (\`${entity.slug}\`)`);
|
|
272
|
+
parts.push('');
|
|
273
|
+
parts.push('### Facts');
|
|
274
|
+
parts.push('');
|
|
275
|
+
if (entity.facts.length === 0) {
|
|
276
|
+
parts.push('None.');
|
|
277
|
+
} else {
|
|
278
|
+
for (const fact of entity.facts) parts.push(renderAuditedFact(fact));
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
parts.push('');
|
|
282
|
+
parts.push('### Owed');
|
|
283
|
+
parts.push('');
|
|
284
|
+
if (entity.owed.length === 0) {
|
|
285
|
+
parts.push('None.');
|
|
286
|
+
} else {
|
|
287
|
+
for (const owed of entity.owed) {
|
|
288
|
+
parts.push(`> **OWED ${owed.id} · ${owed.status}** ${owed.name}`);
|
|
289
|
+
parts.push(`> - why load-bearing: ${owed.why_load_bearing}`);
|
|
290
|
+
parts.push(`> - filled_by: ${owed.filled_by ?? '—'}`);
|
|
291
|
+
parts.push(`> - recorded at ${owed.provenance?.written_at ?? 'unknown'}`);
|
|
292
|
+
parts.push(...renderTraceLines(owed.trace, { prefix: '> - ' }));
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
parts.push('');
|
|
297
|
+
parts.push('### Load links');
|
|
298
|
+
parts.push('');
|
|
299
|
+
if (entity.load_links.length === 0) {
|
|
300
|
+
parts.push('None.');
|
|
301
|
+
} else {
|
|
302
|
+
for (const link of entity.load_links) {
|
|
303
|
+
parts.push(`- **${link.id}** \`${link.removed ? 'removed' : 'active'}\` — fact ${link.fact} carries ${link.carries}`);
|
|
304
|
+
parts.push(...renderRecordedAndRemoval(link));
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return parts.join('\n') + '\n';
|
|
309
|
+
}
|
|
310
|
+
|
|
97
311
|
function renderClaim(claim) {
|
|
98
312
|
const lines = [`- **${claim.id}** \`[${claim.grounding}]\`${claim.owner_locked ? ' `[owner-locked]`' : ''} ${claim.text}`];
|
|
99
313
|
if (Array.isArray(claim.supports) && claim.supports.length > 0) {
|
|
@@ -218,6 +432,7 @@ const EVENT_DETAIL_KEYS = [
|
|
|
218
432
|
'refs', 'rejected', 'conviction', 'trigger', 'open_joints', 'prediction',
|
|
219
433
|
'disposition', 'recall_verdict', 'attribution', 'prediction_ref',
|
|
220
434
|
'prediction_grade', 'reason', 'rank_change', 'elicitation',
|
|
435
|
+
'intent_id', 'tool', 'op', 'rests_on',
|
|
221
436
|
];
|
|
222
437
|
|
|
223
438
|
function renderLedger(snapshot) {
|
|
@@ -248,8 +463,118 @@ function renderLedger(snapshot) {
|
|
|
248
463
|
return parts.join('\n') + '\n';
|
|
249
464
|
}
|
|
250
465
|
|
|
466
|
+
function renderGoalClause(clause) {
|
|
467
|
+
const lines = [`- **${clause.id}** ${clause.text}`];
|
|
468
|
+
lines.push(` - channel: \`${clause.channel}\``);
|
|
469
|
+
if (clause.via !== undefined) lines.push(` - via: ${clause.via}`);
|
|
470
|
+
const citation = clause.elicitation;
|
|
471
|
+
lines.push(
|
|
472
|
+
` - elicitation: asked "${citation.asked}" (answered ${citation.answered_at}, ref ${citation.answer_ref})`,
|
|
473
|
+
);
|
|
474
|
+
lines.push(...renderTraceLines(clause.trace));
|
|
475
|
+
return lines.join('\n');
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function tableCell(value) {
|
|
479
|
+
return String(value).replaceAll('|', '\\|').replaceAll('\n', ' ');
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function renderGoalObjective(snapshot) {
|
|
483
|
+
const chain = snapshot.goalChain ?? [];
|
|
484
|
+
const current = chain.at(-1);
|
|
485
|
+
if (!current) {
|
|
486
|
+
return [
|
|
487
|
+
okfFrontmatter({ type: 'goal', title: 'Objective' }),
|
|
488
|
+
'',
|
|
489
|
+
'# Objective',
|
|
490
|
+
'',
|
|
491
|
+
'No goal recorded.',
|
|
492
|
+
].join('\n') + '\n';
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const state = snapshot.goalState ?? { joints: [], load_links: [] };
|
|
496
|
+
const parts = [okfFrontmatter({
|
|
497
|
+
type: 'goal',
|
|
498
|
+
title: 'Objective',
|
|
499
|
+
timestamp: current.provenance?.written_at,
|
|
500
|
+
})];
|
|
501
|
+
parts.push('');
|
|
502
|
+
parts.push('# Objective');
|
|
503
|
+
parts.push('');
|
|
504
|
+
parts.push(`**Current version:** v${current.version}`);
|
|
505
|
+
const draft = (
|
|
506
|
+
['import', 'migration'].includes(current.provenance?.via)
|
|
507
|
+
&& !current.ratification
|
|
508
|
+
);
|
|
509
|
+
if (draft) {
|
|
510
|
+
parts.push('');
|
|
511
|
+
parts.push('**DRAFT HEALTH WARNING:** This imported/migrated goal is not owner-ratified.');
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
parts.push('');
|
|
515
|
+
parts.push(`## Current clauses (v${current.version})`);
|
|
516
|
+
parts.push('');
|
|
517
|
+
for (const clause of current.clauses) parts.push(renderGoalClause(clause));
|
|
518
|
+
|
|
519
|
+
parts.push('');
|
|
520
|
+
parts.push('## Ratification citation');
|
|
521
|
+
parts.push('');
|
|
522
|
+
if (current.ratification) {
|
|
523
|
+
parts.push(`- asked "${current.ratification.asked}"`);
|
|
524
|
+
parts.push(`- quote: "${current.ratification.quote}"`);
|
|
525
|
+
parts.push(
|
|
526
|
+
`- answered ${current.ratification.answered_at}, ref ${current.ratification.answer_ref}`,
|
|
527
|
+
);
|
|
528
|
+
} else {
|
|
529
|
+
parts.push('None — imported/migrated draft.');
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
parts.push('');
|
|
533
|
+
parts.push('## Goal joints');
|
|
534
|
+
parts.push('');
|
|
535
|
+
if ((state.joints ?? []).length === 0) {
|
|
536
|
+
parts.push('None.');
|
|
537
|
+
} else {
|
|
538
|
+
for (const link of state.joints) {
|
|
539
|
+
parts.push(`- **${link.id}** \`${link.removed ? 'removed' : 'active'}\` — [${link.joint}](REGISTER.md#${link.joint})`);
|
|
540
|
+
parts.push(...renderRecordedAndRemoval(link));
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
parts.push('');
|
|
545
|
+
parts.push('## Load-link bill');
|
|
546
|
+
parts.push('');
|
|
547
|
+
if ((state.load_links ?? []).length === 0) {
|
|
548
|
+
parts.push('None.');
|
|
549
|
+
} else {
|
|
550
|
+
for (const link of state.load_links) {
|
|
551
|
+
const version = Number(/^v([1-9][0-9]*)#/.exec(link.clause)?.[1]);
|
|
552
|
+
const versionMark = version === current.version ? 'current version' : 'superseded version';
|
|
553
|
+
parts.push(`- **${link.id}** \`${link.removed ? 'removed' : 'active'}\` — ${link.clause} \`${versionMark}\` carries ${link.carries}`);
|
|
554
|
+
parts.push(...renderRecordedAndRemoval(link));
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
parts.push('');
|
|
559
|
+
parts.push('## Trajectory');
|
|
560
|
+
parts.push('');
|
|
561
|
+
parts.push('| Version | Date | Provocation | Diff note | Wording fix |');
|
|
562
|
+
parts.push('|---|---|---|---|---|');
|
|
563
|
+
for (const version of chain) {
|
|
564
|
+
const provocation = version.provocation?.quote ?? 'unknown (migrated)';
|
|
565
|
+
const wordingFix = version.clauses.some((clause) => (clause.trace?.length ?? 0) > 0)
|
|
566
|
+
? 'yes'
|
|
567
|
+
: 'no';
|
|
568
|
+
parts.push(
|
|
569
|
+
`| v${version.version} | ${tableCell(version.provenance?.written_at ?? 'unknown')} | ${tableCell(provocation)} | ${tableCell(version.diff_note)} | ${wordingFix} |`,
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
return parts.join('\n') + '\n';
|
|
573
|
+
}
|
|
574
|
+
|
|
251
575
|
function renderObjective(snapshot) {
|
|
252
|
-
|
|
576
|
+
if (snapshot.goalCutoverComplete) return renderGoalObjective(snapshot);
|
|
577
|
+
const objective = (snapshot.positions ?? []).find((p) => p.slug === 'objective');
|
|
253
578
|
if (objective) return renderPosition(objective, snapshot);
|
|
254
579
|
return [
|
|
255
580
|
okfFrontmatter({ type: 'position', title: 'objective' }),
|
|
@@ -261,6 +586,9 @@ function renderObjective(snapshot) {
|
|
|
261
586
|
}
|
|
262
587
|
|
|
263
588
|
function renderIndex(snapshot) {
|
|
589
|
+
const positions = (snapshot.positions ?? []).filter(
|
|
590
|
+
(position) => !(snapshot.goalCutoverComplete && position.slug === 'objective'),
|
|
591
|
+
);
|
|
264
592
|
const parts = [okfFrontmatter({ type: null, title: 'Judgment canon' })];
|
|
265
593
|
parts.push('');
|
|
266
594
|
parts.push('# Judgment canon (generated)');
|
|
@@ -270,11 +598,11 @@ function renderIndex(snapshot) {
|
|
|
270
598
|
parts.push('- [Register](REGISTER.md)');
|
|
271
599
|
parts.push('- [Ledger](LEDGER.md)');
|
|
272
600
|
parts.push('- [Objective](OBJECTIVE.md)');
|
|
273
|
-
if (
|
|
601
|
+
if (positions.length) {
|
|
274
602
|
parts.push('');
|
|
275
603
|
parts.push('## Positions');
|
|
276
604
|
parts.push('');
|
|
277
|
-
for (const p of
|
|
605
|
+
for (const p of positions) {
|
|
278
606
|
parts.push(`- [${p.slug}](positions/${p.slug}.md) — ${p.status}`);
|
|
279
607
|
}
|
|
280
608
|
}
|
|
@@ -293,14 +621,40 @@ export function generateFromRecords(snapshot, { now } = {}) { // eslint-disable-
|
|
|
293
621
|
'docs/judgment/REGISTER.md': renderRegister(snapshot),
|
|
294
622
|
'docs/judgment/LEDGER.md': renderLedger(snapshot),
|
|
295
623
|
'docs/judgment/OBJECTIVE.md': renderObjective(snapshot),
|
|
624
|
+
'docs/judgment/SITUATION.md': renderSituation(snapshot),
|
|
296
625
|
'docs/judgment/index.md': renderIndex(snapshot),
|
|
297
626
|
};
|
|
298
|
-
for (const
|
|
627
|
+
for (const person of [...(snapshot.people ?? [])].sort((a, b) => a.slug.localeCompare(b.slug))) {
|
|
628
|
+
files[`docs/judgment/people/${person.slug}.md`] = renderPerson(person);
|
|
629
|
+
}
|
|
630
|
+
for (const position of (snapshot.positions ?? [])) {
|
|
631
|
+
if (snapshot.goalCutoverComplete && position.slug === 'objective') continue;
|
|
299
632
|
files[`docs/judgment/positions/${position.slug}.md`] = renderPosition(position, snapshot);
|
|
300
633
|
}
|
|
301
634
|
return files;
|
|
302
635
|
}
|
|
303
636
|
|
|
637
|
+
const MANAGED_MARKDOWN_DIRS = [
|
|
638
|
+
'docs/judgment/people',
|
|
639
|
+
'docs/judgment/positions',
|
|
640
|
+
];
|
|
641
|
+
|
|
642
|
+
function managedStaleProjections(cwd, files) {
|
|
643
|
+
const stale = [];
|
|
644
|
+
for (const relDir of MANAGED_MARKDOWN_DIRS) {
|
|
645
|
+
const dir = join(cwd, relDir);
|
|
646
|
+
if (!existsSync(dir)) continue;
|
|
647
|
+
const entries = readdirSync(dir, { withFileTypes: true })
|
|
648
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith('.md'))
|
|
649
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
650
|
+
for (const entry of entries) {
|
|
651
|
+
const rel = `${relDir}/${entry.name}`;
|
|
652
|
+
if (!(rel in files)) stale.push(rel);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
return stale;
|
|
656
|
+
}
|
|
657
|
+
|
|
304
658
|
/**
|
|
305
659
|
* Regenerate every projection from records on disk. Atomic per file.
|
|
306
660
|
* @returns {{ files: string[] }} relative paths written
|
|
@@ -312,6 +666,9 @@ export function regenerateProjections(cwd) {
|
|
|
312
666
|
mkdirSync(join(path, '..'), { recursive: true });
|
|
313
667
|
atomicWrite(path, content);
|
|
314
668
|
}
|
|
669
|
+
for (const rel of managedStaleProjections(cwd, files)) {
|
|
670
|
+
unlinkSync(join(cwd, rel));
|
|
671
|
+
}
|
|
315
672
|
return { files: Object.keys(files) };
|
|
316
673
|
}
|
|
317
674
|
|
|
@@ -325,18 +682,14 @@ export function checkProjectionRoundtrip(cwd) {
|
|
|
325
682
|
const diffs = [];
|
|
326
683
|
for (const [rel, content] of Object.entries(files)) {
|
|
327
684
|
const path = join(cwd, rel);
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
// Orphan detection: a projection file the generator no longer emits (stale
|
|
332
|
-
// position projection, leftover from an older generator) is drift too.
|
|
333
|
-
const positionsDir = join(cwd, 'docs', 'judgment', 'positions');
|
|
334
|
-
if (existsSync(positionsDir)) {
|
|
335
|
-
for (const f of readdirSync(positionsDir)) {
|
|
336
|
-
if (!f.endsWith('.md')) continue;
|
|
337
|
-
const rel = `docs/judgment/positions/${f}`;
|
|
338
|
-
if (!(rel in files)) diffs.push(`${rel} (orphan — not emitted by the generator)`);
|
|
685
|
+
if (!existsSync(path)) {
|
|
686
|
+
diffs.push(`${rel} (missing)`);
|
|
687
|
+
continue;
|
|
339
688
|
}
|
|
689
|
+
if (readFileSync(path, 'utf8') !== content) diffs.push(`${rel} (drift)`);
|
|
690
|
+
}
|
|
691
|
+
for (const rel of managedStaleProjections(cwd, files)) {
|
|
692
|
+
diffs.push(`${rel} (orphan — not emitted by the generator)`);
|
|
340
693
|
}
|
|
341
694
|
return { fixedPoint: diffs.length === 0, diffs };
|
|
342
695
|
}
|
|
@@ -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
|
+
}
|