@smartmemory/compose 0.3.7 → 0.4.0
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 +12 -3
- package/.compose-deps.json +51 -25
- package/README.md +79 -7
- package/bin/compose.js +495 -360
- package/bin/judgment-migrate.js +387 -0
- package/contracts/comp-obs-contract.schema.json +9 -3
- package/contracts/fluid-record.schema.json +209 -0
- package/contracts/lifecycle-backfill.schema.json +322 -0
- package/dist/assets/App-Z4MU-H_F.js +916 -0
- package/dist/assets/{_baseUniq-Bo837sRJ.js → _baseUniq-ClWoCPFl.js} +1 -1
- package/dist/assets/{arc-BafGpyqE.js → arc-DY26UIVo.js} +1 -1
- package/dist/assets/{architectureDiagram-Q4EWVU46-BOBfUsqL.js → architectureDiagram-Q4EWVU46-6Ggq4DqJ.js} +1 -1
- package/dist/assets/{blockDiagram-DXYQGD6D-Dwodev1a.js → blockDiagram-DXYQGD6D-CH3Ked0l.js} +1 -1
- package/dist/assets/{browser-1ntj1-x_.js → browser-BWkrenen.js} +1 -1
- package/dist/assets/{c4Diagram-AHTNJAMY-CU_bhYag.js → c4Diagram-AHTNJAMY-Bk8dYilu.js} +1 -1
- package/dist/assets/channel-SnZzzh7k.js +1 -0
- package/dist/assets/{chunk-4BX2VUAB-p8WsDwnO.js → chunk-4BX2VUAB-BMR0XaAQ.js} +1 -1
- package/dist/assets/{chunk-4TB4RGXK-B8h7-eR0.js → chunk-4TB4RGXK-JytR14a9.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-DxeEr98s.js → chunk-55IACEB6-B4Q97BCP.js} +1 -1
- package/dist/assets/{chunk-EDXVE4YY-BYt8F151.js → chunk-EDXVE4YY-R_qarkSf.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-DGSOVeie.js → chunk-FMBD7UC4-C9s7KR9m.js} +1 -1
- package/dist/assets/{chunk-OYMX7WX6-B-QdgYR2.js → chunk-OYMX7WX6-BySQzVxc.js} +1 -1
- package/dist/assets/{chunk-QZHKN3VN-Du5UAZLs.js → chunk-QZHKN3VN-DdpSYZsW.js} +1 -1
- package/dist/assets/{chunk-YZCP3GAM-C8JbNBSk.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-S5V4N54A-O1ESaqge.js → cose-bilkent-S5V4N54A-BdlU6ZX_.js} +1 -1
- package/dist/assets/{dagre-KV5264BT-CPTmFPHw.js → dagre-KV5264BT-Cp3F5KTn.js} +1 -1
- package/dist/assets/{diagram-5BDNPKRD-B3PNrWs5.js → diagram-5BDNPKRD-DiR6_2q_.js} +1 -1
- package/dist/assets/{diagram-G4DWMVQ6-Cscfr6vc.js → diagram-G4DWMVQ6-w0i-p5HX.js} +1 -1
- package/dist/assets/{diagram-MMDJMWI5-CSfqZ-TM.js → diagram-MMDJMWI5-tIHhwUv3.js} +1 -1
- package/dist/assets/{diagram-TYMM5635-Cg4aYS7W.js → diagram-TYMM5635-BAeY3B19.js} +1 -1
- package/dist/assets/{erDiagram-SMLLAGMA-_ZqwG5pl.js → erDiagram-SMLLAGMA-Ckx_Knko.js} +1 -1
- package/dist/assets/{flowDiagram-DWJPFMVM-C83boxFT.js → flowDiagram-DWJPFMVM-DeoNka6J.js} +1 -1
- package/dist/assets/{ganttDiagram-T4ZO3ILL-CWnIjuEi.js → ganttDiagram-T4ZO3ILL-BmGnFbEg.js} +1 -1
- package/dist/assets/{gitGraphDiagram-UUTBAWPF-DrMdxZfH.js → gitGraphDiagram-UUTBAWPF-Dk48IHsx.js} +1 -1
- package/dist/assets/{graph-RE4I7Ty7.js → graph-BNzKGvoy.js} +1 -1
- package/dist/assets/{graph-Bi99_6Yf.js → graph-CI_1htl0.js} +1 -1
- package/dist/assets/{index-Rm2RE-c0.js → index-BEfrNBp8.js} +3 -3
- package/dist/assets/index-yyrA5OZd.css +1 -0
- package/dist/assets/{infoDiagram-42DDH7IO-BLmP4Epr.js → infoDiagram-42DDH7IO-BRf827i0.js} +1 -1
- package/dist/assets/{ishikawaDiagram-UXIWVN3A-yuWWshKN.js → ishikawaDiagram-UXIWVN3A-0kCZaeCM.js} +1 -1
- package/dist/assets/{journeyDiagram-VCZTEJTY-BOfhaJov.js → journeyDiagram-VCZTEJTY-rvU7ayRt.js} +1 -1
- package/dist/assets/{kanban-definition-6JOO6SKY-Bbolde15.js → kanban-definition-6JOO6SKY-DpQwX1C5.js} +1 -1
- package/dist/assets/{layout-BSf33zm8.js → layout-BI8cXFPI.js} +1 -1
- package/dist/assets/{linear-AvSTWMqx.js → linear-a0glcDiw.js} +1 -1
- package/dist/assets/{min-QBM8H4xN.js → min-vPHfnXcC.js} +1 -1
- package/dist/assets/{mindmap-definition-QFDTVHPH-BuvgtqIc.js → mindmap-definition-QFDTVHPH-D14eF-7C.js} +1 -1
- package/dist/assets/mobile-B7m9EO9D.js +17 -0
- package/dist/assets/{pieDiagram-DEJITSTG-DIzF16vh.js → pieDiagram-DEJITSTG-Cno-gETh.js} +1 -1
- package/dist/assets/{quadrantDiagram-34T5L4WZ-D-mbUIjS.js → quadrantDiagram-34T5L4WZ-BUQM1Hfm.js} +1 -1
- package/dist/assets/{requirementDiagram-MS252O5E-CEs4kCLd.js → requirementDiagram-MS252O5E-pOXlN2-q.js} +1 -1
- package/dist/assets/{sankeyDiagram-XADWPNL6-DFsnCr9n.js → sankeyDiagram-XADWPNL6-Crynd3_b.js} +1 -1
- package/dist/assets/{sequenceDiagram-FGHM5R23-BEJYdTjQ.js → sequenceDiagram-FGHM5R23-D9fZdCM8.js} +1 -1
- package/dist/assets/{stateDiagram-FHFEXIEX-BBXs57uY.js → stateDiagram-FHFEXIEX-CW9qVec8.js} +1 -1
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-DkVLzHbY.js +1 -0
- package/dist/assets/{timeline-definition-GMOUNBTQ-BGvLoVAY.js → timeline-definition-GMOUNBTQ-BcHzhm_8.js} +1 -1
- package/dist/assets/{vennDiagram-DHZGUBPP-9LaBTMe0.js → vennDiagram-DHZGUBPP-BfytJcWk.js} +1 -1
- package/dist/assets/{wardley-RL74JXVD-P4MEqMTP.js → wardley-RL74JXVD-DLj-IjyB.js} +1 -1
- package/dist/assets/{wardleyDiagram-NUSXRM2D-o-tmxnlC.js → wardleyDiagram-NUSXRM2D-Ds0Ue68c.js} +1 -1
- package/dist/assets/{xychartDiagram-5P7HB3ND-Dpn7V6qk.js → xychartDiagram-5P7HB3ND-vjWDXFL6.js} +1 -1
- 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 +9 -0
- package/lib/build-stream-schema.js +3 -1
- package/lib/build-stream-writer.js +25 -0
- package/lib/build.js +874 -170
- package/lib/canon-guard.js +28 -6
- package/lib/canon-override.js +196 -0
- package/lib/canon-registry.js +104 -0
- package/lib/cli-commands.js +144 -0
- package/lib/codex-preflight.js +26 -13
- 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/deps.js +164 -7
- package/lib/dir-lock.js +170 -0
- package/lib/dispatch-ledger.js +3 -3
- 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 +57 -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 -23
- 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/ideabox-cli.js +315 -0
- package/lib/ideabox.js +121 -21
- package/lib/judgment/store/index.js +9 -1
- package/lib/judgment/store/records.js +1 -1
- package/lib/judgment/trace.js +380 -0
- package/lib/judgment-decision-write.js +277 -0
- package/lib/judgment-decisions.js +466 -0
- package/lib/judgment-gen.js +5 -1
- package/lib/judgment-writer.js +56 -2
- package/lib/lifecycle-modes.js +4 -4
- package/lib/lineage.js +400 -0
- package/lib/local-claude-connector.js +52 -1
- package/lib/maya-client.js +302 -0
- package/lib/maya-config.js +53 -0
- package/lib/maya-identity.js +283 -0
- 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 +396 -199
- 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 +271 -36
- 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 +109 -23
- package/server/compose-mcp.js +88 -882
- 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 +34 -2
- 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-PkZzHeMj.js +0 -894
- package/dist/assets/channel-qVK_qn4E.js +0 -1
- package/dist/assets/classDiagram-6PBFFD2Q-B8UcfC1q.js +0 -1
- package/dist/assets/classDiagram-v2-HSJHXN6E-B8UcfC1q.js +0 -1
- package/dist/assets/clone-Pu3RyLUh.js +0 -1
- package/dist/assets/index-LIwREYgH.css +0 -1
- package/dist/assets/mobile-BnXEOE3U.js +0 -17
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-BqKuX4rj.js +0 -1
- package/lib/staleness.js +0 -87
- package/server/ideabox-cache.js +0 -77
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/judgment-decision-write.js — the judgment write path into SmartMemory
|
|
3
|
+
* (GOV-COMPOSE-SEAM-1 step 1 `canon-on-decisions`, phase P2).
|
|
4
|
+
*
|
|
5
|
+
* FAIL-CLOSED, unlike `smartmemory-ingest.js`.
|
|
6
|
+
*
|
|
7
|
+
* Ingest is fire-and-forget on purpose: a dropped feature event costs an
|
|
8
|
+
* analytics row, and the local durable write already happened. A dropped
|
|
9
|
+
* DECISION is different — under D1 the SmartMemory record is the canon, so a
|
|
10
|
+
* silently dropped write leaves a build governed by a rule set that is missing
|
|
11
|
+
* the thing just decided. Nobody sees a warning in a log. So a failed decision
|
|
12
|
+
* write throws, and the caller's tool call fails with it.
|
|
13
|
+
*
|
|
14
|
+
* Three guarantees, in the order they are enforced:
|
|
15
|
+
*
|
|
16
|
+
* 1. GATED — does nothing at all unless `smartmemory.enabled === true`.
|
|
17
|
+
* 2. IDEMPOTENT — a stable key per ledger entry, recorded in ONE local
|
|
18
|
+
* ledger shared by the live path and the backfill, and verified against
|
|
19
|
+
* the service before a skip. **There must be exactly one such ledger.**
|
|
20
|
+
* Two of them briefly existed (this sidecar and the backfill's own
|
|
21
|
+
* `.compose/data/judgment-migration-state.json`), keyed identically but
|
|
22
|
+
* read separately, so a decision written live and then backfilled would
|
|
23
|
+
* be written twice — the create endpoint has no server-side idempotency
|
|
24
|
+
* to catch it. Merged here 2026-08-22; the old file is adopted on first
|
|
25
|
+
* read and then unused.
|
|
26
|
+
* 3. VERIFIED — reads the decision back and checks the provenance survived,
|
|
27
|
+
* rather than trusting a 200. Against a service predating the 2026-08-22
|
|
28
|
+
* field addition, FastAPI silently ignores `source_type` /
|
|
29
|
+
* `context_snapshot`, so a 200 proves nothing about the thing D4 needs.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
33
|
+
import { dirname, join, resolve } from 'node:path';
|
|
34
|
+
|
|
35
|
+
import { getSmartmemoryConfig } from './smartmemory-config.js';
|
|
36
|
+
import { createSmartmemoryClient } from './smartmemory-client.js';
|
|
37
|
+
import { syncManifest } from './judgment-attest.js';
|
|
38
|
+
import { atomicWrite } from './judgment/store/records.js';
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Local idempotency ledger: `idempotency_key -> decision_id`.
|
|
42
|
+
*
|
|
43
|
+
* Lives beside the judgment records because it IS judgment provenance — which
|
|
44
|
+
* ledger entry became which decision. Also serves as the backfill's resume
|
|
45
|
+
* file (P3), which is why it is a file and not process memory.
|
|
46
|
+
*/
|
|
47
|
+
export function sidecarPath(cwd) {
|
|
48
|
+
return join(cwd, 'docs', 'judgment', 'records', 'decision-ids.json');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Decisions created but never verified. Sits beside the sidecar; hand-cleared. */
|
|
52
|
+
export function orphanPath(cwd) {
|
|
53
|
+
return join(cwd, 'docs', 'judgment', 'records', 'decision-orphans.json');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Where the backfill's own resume file used to live before the two idempotency
|
|
58
|
+
* ledgers were merged (2026-08-22). Read once, so a migration already run
|
|
59
|
+
* against the old file is not repeated against the new one.
|
|
60
|
+
*/
|
|
61
|
+
export function legacyStatePath(cwd) {
|
|
62
|
+
return join(cwd, '.compose', 'data', 'judgment-migration-state.json');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function readSidecar(cwd) {
|
|
66
|
+
const path = sidecarPath(cwd);
|
|
67
|
+
if (!existsSync(path)) {
|
|
68
|
+
const legacy = legacyStatePath(cwd);
|
|
69
|
+
if (existsSync(legacy)) {
|
|
70
|
+
try {
|
|
71
|
+
const parsed = JSON.parse(readFileSync(legacy, 'utf8'));
|
|
72
|
+
// Old shape was { version, written: {key: id} }.
|
|
73
|
+
const written = parsed?.written;
|
|
74
|
+
if (written && typeof written === 'object') return { ...written };
|
|
75
|
+
} catch {
|
|
76
|
+
throw new Error(
|
|
77
|
+
`judgment-decision-write: the legacy resume file ${legacy} is unreadable. `
|
|
78
|
+
+ 'Repair or delete it deliberately; ignoring it would re-write every decision '
|
|
79
|
+
+ 'the earlier backfill already stored.',
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return {};
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
87
|
+
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
88
|
+
} catch {
|
|
89
|
+
// A corrupt sidecar must not silently become "nothing was ever written" —
|
|
90
|
+
// that would re-write every decision on the next run.
|
|
91
|
+
throw new Error(
|
|
92
|
+
`judgment-decision-write: ${path} is unreadable. Repair or delete it deliberately; `
|
|
93
|
+
+ 'treating it as empty would duplicate every decision already written.',
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Both ledgers live UNDER `docs/judgment/records/`, so every byte written here
|
|
100
|
+
* is attested: `recordFileSet` walks that tree wholesale and the drift detector
|
|
101
|
+
* reports anything the manifest does not remember. Writing without stamping
|
|
102
|
+
* leaves the canon in permanent `[added]` drift and blocks every later commit
|
|
103
|
+
* and push — which is exactly what the first real backfill did (2026-08-23).
|
|
104
|
+
*
|
|
105
|
+
* Stamping is the sanctioned writer path (`lib/judgment-writer.js` does the same
|
|
106
|
+
* after every publication), not a way around the guard: it records what THIS
|
|
107
|
+
* writer wrote, so a later hand-edit of the ledger still surfaces as drift.
|
|
108
|
+
*/
|
|
109
|
+
function stampLedger(cwd, path) {
|
|
110
|
+
syncManifest(resolve(cwd), [path]);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function writeSidecar(cwd, map) {
|
|
114
|
+
const path = sidecarPath(cwd);
|
|
115
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
116
|
+
atomicWrite(path, `${JSON.stringify(map, null, 2)}\n`);
|
|
117
|
+
stampLedger(cwd, path);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Did the provenance actually land?
|
|
122
|
+
*
|
|
123
|
+
* Checks the two fields the feature turns on, not the whole payload. A service
|
|
124
|
+
* that ignored them answers 200 and stores a decision whose conviction
|
|
125
|
+
* provenance is simply absent — indistinguishable, from the caller's side, from
|
|
126
|
+
* a successful write, which is the exact failure D4 exists to prevent.
|
|
127
|
+
*/
|
|
128
|
+
/**
|
|
129
|
+
* Note a decision that was created but could not be verified.
|
|
130
|
+
*
|
|
131
|
+
* Kept beside the sidecar and NEVER auto-deleted. A create that we then failed
|
|
132
|
+
* to verify is exactly the case where we do not know what the service stored,
|
|
133
|
+
* and an automated delete there can destroy a good record on a bad diagnosis.
|
|
134
|
+
* Recording it means the next run can report the duplicate rather than the
|
|
135
|
+
* duplicate going unnoticed forever.
|
|
136
|
+
*/
|
|
137
|
+
export function recordOrphan(cwd, entry) {
|
|
138
|
+
const p = orphanPath(cwd);
|
|
139
|
+
const existing = existsSync(p) ? JSON.parse(readFileSync(p, 'utf8')) : { version: 1, orphans: [] };
|
|
140
|
+
existing.orphans.push(entry);
|
|
141
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
142
|
+
writeFileSync(p, `${JSON.stringify(existing, null, 2)}\n`);
|
|
143
|
+
stampLedger(cwd, p);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function provenanceLanded(stored, sent) {
|
|
147
|
+
if (!stored) return false;
|
|
148
|
+
if (sent.source_type !== undefined && stored.source_type !== sent.source_type) return false;
|
|
149
|
+
if (sent.context_snapshot !== undefined) {
|
|
150
|
+
const got = stored.context_snapshot;
|
|
151
|
+
if (!got || typeof got !== 'object') return false;
|
|
152
|
+
// Key-wise, not deep-equal: the lifecycle owns three reserved slots inside
|
|
153
|
+
// context_snapshot and may add them (CORE-SUPERSEDE-NOTE-1), so a strict
|
|
154
|
+
// equality check would fail on a correct write.
|
|
155
|
+
for (const [k, v] of Object.entries(sent.context_snapshot)) {
|
|
156
|
+
// A sent `null` comes back ABSENT: the store drops null-valued keys from
|
|
157
|
+
// context_snapshot (measured 2026-08-22 — every key of a 13-key snapshot
|
|
158
|
+
// round-tripped except `ledger_anchor`, the only null). The store cannot
|
|
159
|
+
// represent the difference, so "sent null" and "stored absent" are the
|
|
160
|
+
// same fact and must not read as a lost write. Anything else still has
|
|
161
|
+
// to match exactly.
|
|
162
|
+
if (v === null && got[k] === undefined) continue;
|
|
163
|
+
if (JSON.stringify(got[k]) !== JSON.stringify(v)) return false;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Write one mapped decision, once.
|
|
171
|
+
*
|
|
172
|
+
* @param {string} cwd
|
|
173
|
+
* @param {object} decision payload from `ledgerEntryToDecision`
|
|
174
|
+
* @param {{client?: object, config?: object}} [deps] injection seam for tests
|
|
175
|
+
* @returns {Promise<{decision_id: string, skipped: boolean}|null>} null when the
|
|
176
|
+
* coupling is off — the ONLY silent no-op, and it is a configuration state,
|
|
177
|
+
* not a failure.
|
|
178
|
+
*/
|
|
179
|
+
export async function writeJudgmentDecision(cwd, decision, deps = {}) {
|
|
180
|
+
const cfg = deps.config ?? getSmartmemoryConfig(cwd);
|
|
181
|
+
if (cfg.enabled !== true) return null;
|
|
182
|
+
|
|
183
|
+
const key = decision.idempotency_key;
|
|
184
|
+
if (!key) {
|
|
185
|
+
throw new Error('judgment-decision-write: decision has no idempotency_key; refusing to write');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const sidecar = readSidecar(cwd);
|
|
189
|
+
const client = deps.client ?? createSmartmemoryClient(cfg);
|
|
190
|
+
|
|
191
|
+
const known = sidecar[key];
|
|
192
|
+
if (known) {
|
|
193
|
+
// Idempotency is VERIFIED, not assumed. A key in the local ledger only
|
|
194
|
+
// counts once the decision is confirmed still present server-side —
|
|
195
|
+
// otherwise a ledger that has drifted from the service (restored backup,
|
|
196
|
+
// deleted decision, wrong workspace) silently skips a write that never
|
|
197
|
+
// landed, and the gap is invisible forever after.
|
|
198
|
+
const live = await client.getDecision(known);
|
|
199
|
+
if (live) return { decision_id: known, skipped: true };
|
|
200
|
+
process.stderr.write(
|
|
201
|
+
`[judgment-decision-write] ledger claimed ${key} -> ${known} but the service has no `
|
|
202
|
+
+ 'such decision; rewriting.\n',
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// `idempotency_key` and `supersedes_slug` are mapper-internal and are not
|
|
207
|
+
// fields on the create contract. The key travels inside context_snapshot so
|
|
208
|
+
// the remote record can be reconciled against the sidecar if they diverge.
|
|
209
|
+
const { idempotency_key: _k, supersedes_slug: _s, status: intendedStatus, ...rest } = decision;
|
|
210
|
+
|
|
211
|
+
// `status` is NOT a field on the create contract, and there is no route that
|
|
212
|
+
// can write a decision with both a non-active lifecycle state and its
|
|
213
|
+
// provenance: `/decisions/create` has no `status`, and `/decisions/pending/create`
|
|
214
|
+
// accepts no `source_type`, `context_snapshot`, `confidence` or `rationale`.
|
|
215
|
+
// A `pending` decision therefore lands `active` whichever route is used, and
|
|
216
|
+
// the choice is between a wrong status and lost provenance.
|
|
217
|
+
//
|
|
218
|
+
// Provenance wins, and the divergence is made LOUD rather than dropped: the
|
|
219
|
+
// intent is recorded on the record itself so it is visible to anyone reading
|
|
220
|
+
// the decision, and warned once per write so it is visible to whoever ran the
|
|
221
|
+
// migration. Closing it needs a service change (a `status` on create, or the
|
|
222
|
+
// pending route accepting provenance) — see the P3 migration report.
|
|
223
|
+
const statusDiverged = intendedStatus !== undefined && intendedStatus !== 'active';
|
|
224
|
+
if (statusDiverged) {
|
|
225
|
+
process.stderr.write(
|
|
226
|
+
`[judgment-decision-write] ${key}: intended status "${intendedStatus}" cannot be written — `
|
|
227
|
+
+ 'no route accepts a lifecycle state together with provenance. Landing as "active" with '
|
|
228
|
+
+ 'intended_status recorded on the record.\n',
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const payload = {
|
|
233
|
+
...rest,
|
|
234
|
+
context_snapshot: {
|
|
235
|
+
...(decision.context_snapshot ?? {}),
|
|
236
|
+
idempotency_key: key,
|
|
237
|
+
...(statusDiverged ? { intended_status: intendedStatus, status_diverged: true } : {}),
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
const created = await client.createDecision(payload);
|
|
242
|
+
const stored = await client.getDecision(created.decision_id);
|
|
243
|
+
|
|
244
|
+
if (!provenanceLanded(stored, payload)) {
|
|
245
|
+
// The decision EXISTS server-side at this point and is not going to be
|
|
246
|
+
// recorded in the sidecar, so a later re-run would write a second copy of
|
|
247
|
+
// the same ledger entry. Record it as an orphan so the next run reports it
|
|
248
|
+
// instead of silently duplicating. Not auto-deleted: deleting is
|
|
249
|
+
// irreversible and this path fires precisely when we do not understand
|
|
250
|
+
// what the service did with the write.
|
|
251
|
+
recordOrphan(cwd, {
|
|
252
|
+
key,
|
|
253
|
+
decision_id: created.decision_id,
|
|
254
|
+
at: stored ? 'provenance-mismatch' : 'read-back-empty',
|
|
255
|
+
});
|
|
256
|
+
throw new Error(
|
|
257
|
+
`judgment-decision-write: ${created.decision_id} was created but its provenance did not land. `
|
|
258
|
+
+ 'The service is probably older than the 2026-08-22 source_type/context_snapshot change, '
|
|
259
|
+
+ 'which FastAPI ignores silently. Failing rather than recording a decision whose '
|
|
260
|
+
+ 'conviction provenance is absent. The created decision is recorded as an orphan; '
|
|
261
|
+
+ 'delete it by hand once the cause is understood.',
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Remote first, sidecar second. The reverse would leave a phantom entry when
|
|
266
|
+
// the write fails, permanently skipping a decision that was never stored.
|
|
267
|
+
//
|
|
268
|
+
// KNOWN WINDOW: a crash between the two duplicates this one decision on the
|
|
269
|
+
// next run. Stated rather than hidden — closing it needs a server-side
|
|
270
|
+
// idempotency key, which the create contract does not have. The remote record
|
|
271
|
+
// carries `context_snapshot.idempotency_key`, so a duplicate is detectable
|
|
272
|
+
// and repairable after the fact.
|
|
273
|
+
sidecar[key] = created.decision_id;
|
|
274
|
+
writeSidecar(cwd, sidecar);
|
|
275
|
+
|
|
276
|
+
return { decision_id: created.decision_id, skipped: false };
|
|
277
|
+
}
|