@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,466 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/judgment-decisions.js — ledger event to SmartMemory decision mapping
|
|
3
|
+
* (GOV-COMPOSE-SEAM-1 step 1 `canon-on-decisions`, phase P1).
|
|
4
|
+
*
|
|
5
|
+
* Pure. No I/O, no clock, no SmartMemory client. The write path (P2) and the
|
|
6
|
+
* backfill (P3) both call these; the dry run (`bin/judgment-migrate.js`) calls
|
|
7
|
+
* them and writes nothing, which is what makes the value spike measurable
|
|
8
|
+
* before anything is committed.
|
|
9
|
+
*
|
|
10
|
+
* Design answers this file implements — see
|
|
11
|
+
* smart-memory-docs/docs/features/GOV-COMPOSE-SEAM-1/design.md:
|
|
12
|
+
* D1 SmartMemory owns; markdown is a projection.
|
|
13
|
+
* D3 kind mapping (below).
|
|
14
|
+
* D4 an inferred conviction may NOT produce the same confidence as a stated
|
|
15
|
+
* one, and the distinction survives onto `source_type`.
|
|
16
|
+
* D5 enforceable = names a step + names an observable + a build could
|
|
17
|
+
* violate it. Fixed in writing before the count was taken.
|
|
18
|
+
*
|
|
19
|
+
* P2.5 (D4 review gate) is applied by `applyConvictionReview`, from the owner
|
|
20
|
+
* verdicts in `docs/features/GOV-COMPOSE-SEAM-1/conviction-review.json`. The
|
|
21
|
+
* backfill must call it and must refuse any inferred conviction the file does
|
|
22
|
+
* not rule on.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { createHash } from 'node:crypto';
|
|
26
|
+
|
|
27
|
+
/** Ledger kinds that become a SmartMemory decision at all (D3). */
|
|
28
|
+
export const DECISION_KINDS = new Set(['decide', 'kill', 'open', 'correct']);
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Kinds that touch a decision without being one. `calibrate` amends an existing
|
|
32
|
+
* decision's confidence; the rest are annotation, routing or integrity events
|
|
33
|
+
* and must not inflate the decision store (D3).
|
|
34
|
+
*/
|
|
35
|
+
export const NON_DECISION_KINDS = new Set(['note', 'escalate', 'override', 'attest', 'calibrate']);
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Conviction to confidence.
|
|
39
|
+
*
|
|
40
|
+
* Two separate scales on purpose (D4). A conviction the owner stated is worth
|
|
41
|
+
* more than one an agent inferred from a transcript, and the gap has to be
|
|
42
|
+
* visible in the number as well as in `source_type` — otherwise a guess reads
|
|
43
|
+
* as an assertion the moment it lands in a bundle rule.
|
|
44
|
+
*
|
|
45
|
+
* An inferred `high` (0.55) sits BELOW a stated `medium` (0.6) deliberately: we
|
|
46
|
+
* would rather act on something the owner actually said with middling
|
|
47
|
+
* conviction than on something we decided he probably meant.
|
|
48
|
+
*/
|
|
49
|
+
export const CONFIDENCE = {
|
|
50
|
+
stated: { high: 0.9, medium: 0.6, low: 0.35 },
|
|
51
|
+
inferred: { high: 0.55, medium: 0.4, low: 0.25 },
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/** Confidence for a decision-shaped entry that carries no conviction at all. */
|
|
55
|
+
export const CONFIDENCE_UNSTATED = 0.5;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Stable identity for a ledger entry.
|
|
59
|
+
*
|
|
60
|
+
* The jsonl carries no id — identity is position plus content. Hashing
|
|
61
|
+
* (seq, kind, title, written_at) gives the backfill an idempotency key that
|
|
62
|
+
* survives a re-run without a schema change, and changes if the entry is
|
|
63
|
+
* rewritten, which is what we want: a rewritten entry is a different fact.
|
|
64
|
+
*/
|
|
65
|
+
export function stableEntryKey(entry, seq) {
|
|
66
|
+
const material = [
|
|
67
|
+
String(seq),
|
|
68
|
+
entry.kind ?? '',
|
|
69
|
+
entry.title ?? '',
|
|
70
|
+
entry.provenance?.written_at ?? '',
|
|
71
|
+
].join(' ');
|
|
72
|
+
return `compose-ledger-${createHash('sha256').update(material).digest('hex').slice(0, 16)}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Split a ledger title of the form `slug — prose` into its two halves.
|
|
77
|
+
* Both em dash and hyphen separators appear in the imported canon.
|
|
78
|
+
*/
|
|
79
|
+
export function splitTitle(title = '') {
|
|
80
|
+
const m = title.match(/^([a-z0-9][a-z0-9-]*)\s+[—-]\s+([\s\S]*)$/);
|
|
81
|
+
if (!m) return { slug: null, statement: (title || '').trim() };
|
|
82
|
+
return { slug: m[1], statement: m[2].trim() };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* `decide` to `policy` when the entry states a forward-going rule, else
|
|
87
|
+
* `choice`.
|
|
88
|
+
*
|
|
89
|
+
* This is the D3 split that feeds the spike, so it is deliberately narrow: a
|
|
90
|
+
* policy has to read as an instruction that outlives the moment. Anything
|
|
91
|
+
* ambiguous stays a `choice`, which counts for less. Erring the other way would
|
|
92
|
+
* be the motivated counting the plan warns about.
|
|
93
|
+
*/
|
|
94
|
+
const POLICY_MARKERS = [
|
|
95
|
+
/\balways\b/i,
|
|
96
|
+
/\bnever\b/i,
|
|
97
|
+
/\bmust\b/i,
|
|
98
|
+
/\bfrom (?:here|now) on\b/i,
|
|
99
|
+
/\bgoing forward\b/i,
|
|
100
|
+
/\bby default\b/i,
|
|
101
|
+
/\bno new\b/i,
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
export function decideSubtype(entry) {
|
|
105
|
+
const text = `${entry.title ?? ''}\n${entry.body ?? ''}`;
|
|
106
|
+
return POLICY_MARKERS.some((re) => re.test(text)) ? 'policy' : 'choice';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* D5 — the enforceability test, applied as three independent signals.
|
|
111
|
+
*
|
|
112
|
+
* Returns the signals, not just a verdict, because the reported spike number is
|
|
113
|
+
* an ADJUDICATED count: this classifier proposes, a human confirms. A regex
|
|
114
|
+
* cannot tell "reviews run before merge" from "reviewing is how we work", and
|
|
115
|
+
* pretending otherwise would produce exactly the motivated number D5 exists to
|
|
116
|
+
* prevent. `verdict: 'candidate'` means "bring this one to the adjudication
|
|
117
|
+
* pass", never "counted".
|
|
118
|
+
*/
|
|
119
|
+
const STEP_MARKERS = [
|
|
120
|
+
/\b(?:design|blueprint|implement|review|merge|commit|push|deploy|release|ingest|recall|migrate|build|test)\b/i,
|
|
121
|
+
/\bstep\b/i,
|
|
122
|
+
/\bgate\b/i,
|
|
123
|
+
/\bpipeline\b/i,
|
|
124
|
+
];
|
|
125
|
+
|
|
126
|
+
const OBSERVABLE_MARKERS = [
|
|
127
|
+
/\bfiles?\b/i,
|
|
128
|
+
/\bstatus\b/i,
|
|
129
|
+
/\bcount\b/i,
|
|
130
|
+
/\bexit code\b/i,
|
|
131
|
+
/\btests?\b/i,
|
|
132
|
+
/\bimports?\b/i,
|
|
133
|
+
/\bcontract\b/i,
|
|
134
|
+
/\bschema\b/i,
|
|
135
|
+
/\bversion\b/i,
|
|
136
|
+
/\bcommit\b/i,
|
|
137
|
+
/\bfields?\b/i,
|
|
138
|
+
];
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* A rule true by construction cannot be violated, so it is not enforceable —
|
|
142
|
+
* it is a description of how the system already works. These read as claims
|
|
143
|
+
* about identity or belief rather than constraints on a run.
|
|
144
|
+
*/
|
|
145
|
+
const NON_VIOLABLE_MARKERS = [
|
|
146
|
+
/\bis what\b/i,
|
|
147
|
+
/\bthe product is\b/i,
|
|
148
|
+
/\bwe are\b/i,
|
|
149
|
+
/\bthe point is\b/i,
|
|
150
|
+
/\bframing\b/i,
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
export function classifyEnforceable(entry) {
|
|
154
|
+
const text = `${entry.title ?? ''}\n${entry.body ?? ''}`;
|
|
155
|
+
const namesStep = STEP_MARKERS.some((re) => re.test(text));
|
|
156
|
+
const namesObservable = OBSERVABLE_MARKERS.some((re) => re.test(text));
|
|
157
|
+
const violable = !NON_VIOLABLE_MARKERS.some((re) => re.test(text));
|
|
158
|
+
const signals = { namesStep, namesObservable, violable };
|
|
159
|
+
const all = namesStep && namesObservable && violable;
|
|
160
|
+
return {
|
|
161
|
+
signals,
|
|
162
|
+
// Three-way on purpose. `historical` is a confident no; `candidate` is
|
|
163
|
+
// "all three signals fired, a human must confirm"; nothing here is ever a
|
|
164
|
+
// confident yes, because the yes is the judgement the spike is measuring.
|
|
165
|
+
verdict: all ? 'candidate' : 'historical',
|
|
166
|
+
adjudicated: null,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* One rejected alternative, as the single string the contract accepts.
|
|
172
|
+
*
|
|
173
|
+
* The ledger models a rejection as `{what, why}`; `rejected_alternatives` is
|
|
174
|
+
* `array<string>` end to end. The `why` is kept in the same string rather than
|
|
175
|
+
* dropped — a rejected option without its reason is the least useful half.
|
|
176
|
+
*/
|
|
177
|
+
export function flattenRejected(r) {
|
|
178
|
+
if (typeof r === 'string') return r;
|
|
179
|
+
const what = (r?.what ?? '').trim();
|
|
180
|
+
const why = (r?.why ?? '').trim();
|
|
181
|
+
if (!what) return why;
|
|
182
|
+
return why ? `${what} — ${why}` : what;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Map one ledger event to a SmartMemory decision payload (D3).
|
|
187
|
+
*
|
|
188
|
+
* Returns `null` for kinds that are not decisions — callers filter, they do not
|
|
189
|
+
* branch on kind themselves, so the D3 table has exactly one implementation.
|
|
190
|
+
*
|
|
191
|
+
* `correct` maps to a supersede, which needs a target that only the backfill
|
|
192
|
+
* knows (the decision written for the entry it corrects). The payload therefore
|
|
193
|
+
* carries `supersedes_slug` and the caller resolves it; mapping it here would
|
|
194
|
+
* require I/O and this file stays pure.
|
|
195
|
+
*/
|
|
196
|
+
export function ledgerEntryToDecision(entry, seq) {
|
|
197
|
+
if (!entry || typeof entry !== 'object') return null;
|
|
198
|
+
const kind = entry.kind;
|
|
199
|
+
if (!DECISION_KINDS.has(kind)) return null;
|
|
200
|
+
|
|
201
|
+
const { slug, statement } = splitTitle(entry.title ?? '');
|
|
202
|
+
const conviction = entry.conviction ?? null;
|
|
203
|
+
const source = conviction?.source === 'stated'
|
|
204
|
+
? 'stated'
|
|
205
|
+
: conviction?.source === 'inferred' ? 'inferred' : null;
|
|
206
|
+
const level = conviction?.level ?? null;
|
|
207
|
+
|
|
208
|
+
const confidence = source && level && CONFIDENCE[source]?.[level] !== undefined
|
|
209
|
+
? CONFIDENCE[source][level]
|
|
210
|
+
: CONFIDENCE_UNSTATED;
|
|
211
|
+
|
|
212
|
+
// D4: the guess/assertion distinction survives onto the decision itself, not
|
|
213
|
+
// only into the confidence number, so a bundle rule built on a guess is
|
|
214
|
+
// visibly built on a guess wherever it is read.
|
|
215
|
+
const sourceType = source === 'stated'
|
|
216
|
+
? 'explicit'
|
|
217
|
+
: source === 'inferred' ? 'inferred' : 'imported';
|
|
218
|
+
|
|
219
|
+
const decision = {
|
|
220
|
+
idempotency_key: stableEntryKey(entry, seq),
|
|
221
|
+
content: statement,
|
|
222
|
+
decision_type: kind === 'decide' ? decideSubtype(entry) : 'choice',
|
|
223
|
+
confidence,
|
|
224
|
+
source_type: sourceType,
|
|
225
|
+
status: kind === 'open' ? 'pending' : 'active',
|
|
226
|
+
domain: 'compose',
|
|
227
|
+
tags: ['compose-judgment', `ledger:${kind}`, ...(slug ? [`slug:${slug}`] : [])],
|
|
228
|
+
rationale: entry.body ?? '',
|
|
229
|
+
rejected_alternatives: [],
|
|
230
|
+
context_snapshot: {
|
|
231
|
+
ledger_seq: seq,
|
|
232
|
+
ledger_kind: kind,
|
|
233
|
+
ledger_slug: slug,
|
|
234
|
+
ledger_anchor: entry.anchor ?? null,
|
|
235
|
+
// `refs` is a RENDERED field: judgment-gen's EVENT_DETAIL_KEYS prints it
|
|
236
|
+
// into LEDGER.md. Dropping it made the markdown unreproducible from the
|
|
237
|
+
// decision store — measured 2026-08-23, 14 of the 43 backfilled entries
|
|
238
|
+
// carry refs and every one of them lost its evidence pointers. It is the
|
|
239
|
+
// ONLY rendered detail key the mapper did not carry; `rejected` and
|
|
240
|
+
// `conviction` already travel, and no decision-shaped entry uses any of
|
|
241
|
+
// the other fourteen. Empty stays empty rather than `[]` so the snapshot
|
|
242
|
+
// does not grow a key for the 29 entries that have no refs.
|
|
243
|
+
...(Array.isArray(entry.refs) && entry.refs.length ? { ledger_refs: entry.refs } : {}),
|
|
244
|
+
// The STRUCTURED rejected list, beside the flattened one.
|
|
245
|
+
//
|
|
246
|
+
// `rejected_alternatives` is `list[str]` on the wire, so `{what, why}` is
|
|
247
|
+
// joined with " — ". That join is not reversible: the separator also
|
|
248
|
+
// occurs INSIDE `what` (an em dash is ordinary prose), and splitting on
|
|
249
|
+
// the first or the last occurrence both guess wrong. Measured 2026-08-23:
|
|
250
|
+
// 8 of the 43 backfilled entries reconstruct to the wrong what/why split,
|
|
251
|
+
// which renders a different LEDGER.md line.
|
|
252
|
+
//
|
|
253
|
+
// `context_snapshot` has no such type constraint, so the original travels
|
|
254
|
+
// intact here while the flattened list stays where readers expect it.
|
|
255
|
+
// This is NOT the same data twice over: one is for a human reading a
|
|
256
|
+
// decision, the other is what the projection has to reproduce.
|
|
257
|
+
...(Array.isArray(entry.rejected) && entry.rejected.length
|
|
258
|
+
? { ledger_rejected: entry.rejected }
|
|
259
|
+
: {}),
|
|
260
|
+
written_at: entry.provenance?.written_at ?? null,
|
|
261
|
+
via: entry.provenance?.via ?? null,
|
|
262
|
+
actor: entry.provenance?.actor ?? null,
|
|
263
|
+
conviction,
|
|
264
|
+
// D4 review gate: an inferred conviction is not written at stated
|
|
265
|
+
// confidence until the owner has ruled on it. The backfill REFUSES on
|
|
266
|
+
// this flag rather than warning (no silent degradation).
|
|
267
|
+
conviction_review_required: source === 'inferred',
|
|
268
|
+
conviction_review: null,
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
// `rejected_alternatives` is `array<string>` on both the core manager and the
|
|
273
|
+
// HTTP contract — there is NO field for the reason. Flattening "what (why)"
|
|
274
|
+
// into one string is lossy-but-visible; dropping the why silently would be
|
|
275
|
+
// lossy-and-invisible, and the why is the half a reader actually needs.
|
|
276
|
+
// See docs/features/GOV-COMPOSE-SEAM-1/decision-create-contract.json.
|
|
277
|
+
if (Array.isArray(entry.rejected)) {
|
|
278
|
+
decision.rejected_alternatives = entry.rejected.map((r) => flattenRejected(r));
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// A kill is a live decision NOT to do something, so it stays `active` with
|
|
282
|
+
// the killed option recorded as the rejected alternative (D3). Mapping it to
|
|
283
|
+
// `abandoned` would say the decision itself was dropped, which is the
|
|
284
|
+
// opposite of what a kill means.
|
|
285
|
+
if (kind === 'kill') {
|
|
286
|
+
decision.rejected_alternatives.unshift(
|
|
287
|
+
flattenRejected({ what: statement, why: entry.reason ?? entry.body ?? '' }),
|
|
288
|
+
);
|
|
289
|
+
decision.content = `Do not: ${statement}`;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (kind === 'correct') {
|
|
293
|
+
decision.supersedes_slug = slug;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return decision;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* The inverse of `ledgerEntryToDecision` (GOV-COMPOSE-SEAM-1 P4).
|
|
301
|
+
*
|
|
302
|
+
* D1 says SmartMemory owns the canon and the markdown is a generated
|
|
303
|
+
* projection. That is only true if a decision can be turned back into the
|
|
304
|
+
* ledger event `judgment-gen.js` renders — otherwise the markdown is still
|
|
305
|
+
* authoritative and D1 was never really settled.
|
|
306
|
+
*
|
|
307
|
+
* Everything this needs is read from `context_snapshot`, which is the only
|
|
308
|
+
* field on the create contract with no shape constraint. `content`, `rationale`
|
|
309
|
+
* and `rejected_alternatives` are for a human reading a decision; they are
|
|
310
|
+
* derived views (a kill's content is prefixed, its alternatives are flattened)
|
|
311
|
+
* and are deliberately NOT the reconstruction source.
|
|
312
|
+
*
|
|
313
|
+
* What this CANNOT recover, and why:
|
|
314
|
+
* - `provenance.session` is never sent forward, so it comes back `null`. It
|
|
315
|
+
* is `null` on every entry in the current ledger and does not render.
|
|
316
|
+
* - Non-decision kinds (`note`, `escalate`, `override`, `calibrate`,
|
|
317
|
+
* `attest`) were ruled out of the decision store by D3 and are not in
|
|
318
|
+
* SmartMemory at all. 73 of the 116 ledger entries are in that class, so
|
|
319
|
+
* LEDGER.md as a whole does NOT regenerate from decisions — only its
|
|
320
|
+
* decision-shaped events do. See the P4 section of the plan.
|
|
321
|
+
*
|
|
322
|
+
* @param {object} decision a decision as the service returns it
|
|
323
|
+
* @returns {{ seq: number|null, entry: object }|null}
|
|
324
|
+
*/
|
|
325
|
+
export function decisionToLedgerEntry(decision) {
|
|
326
|
+
const cs = decision?.context_snapshot;
|
|
327
|
+
if (!cs || typeof cs !== 'object') return null;
|
|
328
|
+
const kind = cs.ledger_kind;
|
|
329
|
+
if (!DECISION_KINDS.has(kind)) return null;
|
|
330
|
+
|
|
331
|
+
const slug = cs.ledger_slug ?? null;
|
|
332
|
+
// A kill's content is stored prefixed so it reads as a decision NOT to do
|
|
333
|
+
// something; the ledger title carries the bare statement.
|
|
334
|
+
const statement = kind === 'kill'
|
|
335
|
+
? String(decision.content ?? '').replace(/^Do not: /, '')
|
|
336
|
+
: String(decision.content ?? '');
|
|
337
|
+
|
|
338
|
+
const entry = {
|
|
339
|
+
provenance: {
|
|
340
|
+
actor: cs.actor ?? null,
|
|
341
|
+
session: null,
|
|
342
|
+
written_at: cs.written_at ?? null,
|
|
343
|
+
// `via` is omitted rather than null when absent: the store drops null
|
|
344
|
+
// values from context_snapshot, and the renderer prints a `· via x`
|
|
345
|
+
// suffix only for a truthy value, so a null key would be noise.
|
|
346
|
+
...(cs.via ? { via: cs.via } : {}),
|
|
347
|
+
},
|
|
348
|
+
kind,
|
|
349
|
+
title: slug ? `${slug} — ${statement}` : statement,
|
|
350
|
+
body: decision.rationale ?? '',
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
if (cs.ledger_refs) entry.refs = cs.ledger_refs;
|
|
354
|
+
// The structured list, never the flattened one: `flattenRejected` is not
|
|
355
|
+
// reversible (the " — " join also occurs inside `what`).
|
|
356
|
+
if (cs.ledger_rejected) entry.rejected = cs.ledger_rejected;
|
|
357
|
+
if (cs.conviction) entry.conviction = cs.conviction;
|
|
358
|
+
if (cs.ledger_anchor) entry.anchor = cs.ledger_anchor;
|
|
359
|
+
|
|
360
|
+
return { seq: cs.ledger_seq ?? null, entry };
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** Read a whole ledger stream into mapped decisions plus the skipped tail. */
|
|
364
|
+
export function mapLedger(events) {
|
|
365
|
+
const decisions = [];
|
|
366
|
+
const skipped = [];
|
|
367
|
+
events.forEach((entry, i) => {
|
|
368
|
+
const seq = i + 1;
|
|
369
|
+
const mapped = ledgerEntryToDecision(entry, seq);
|
|
370
|
+
if (mapped) {
|
|
371
|
+
decisions.push({
|
|
372
|
+
seq,
|
|
373
|
+
entry,
|
|
374
|
+
decision: mapped,
|
|
375
|
+
enforceability: classifyEnforceable(entry),
|
|
376
|
+
});
|
|
377
|
+
} else {
|
|
378
|
+
skipped.push({ seq, kind: entry?.kind ?? null, title: entry?.title ?? '' });
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
return { decisions, skipped };
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// ── P2.5: the owner's inferred-conviction verdicts (D4) ────────────────────
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* The three verdicts the owner ruled on 2026-08-22. The 15 inferred entries
|
|
388
|
+
* sorted cleanly by what evidence of the owner they actually contain, so they
|
|
389
|
+
* were ruled as three groups rather than one at a time.
|
|
390
|
+
*
|
|
391
|
+
* - `promoted` (group A) — the entry quotes the owner's own words. The
|
|
392
|
+
* `inferred` label was simply wrong: someone wrote down what he said and then
|
|
393
|
+
* filed the confidence as a guess. Migrates at the stated scale.
|
|
394
|
+
* - `choice_kept_strength_dropped` (group B) — the owner made the decision
|
|
395
|
+
* ("Decided (owner, elicited)") but the CONFIDENCE was read off behaviour:
|
|
396
|
+
* "without hedging", "quick, unhesitating yes", "no elaboration offered".
|
|
397
|
+
* The choice is kept and attributed; the agent's reading of its strength is
|
|
398
|
+
* discarded. A fast yes can mean confident or can mean bored, and treating
|
|
399
|
+
* the two as the same is the laundering D4 exists to stop.
|
|
400
|
+
* - `unrated` (group C) — no trace the owner was ever asked. Migrates so the
|
|
401
|
+
* history survives, but carrying an explicit "nobody has ruled on this"
|
|
402
|
+
* marker, and may not back an enforceable rule until someone does.
|
|
403
|
+
*/
|
|
404
|
+
export const REVIEW_VERDICTS = new Set(['promoted', 'choice_kept_strength_dropped', 'unrated']);
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* `Decision.confidence` is a non-optional `float` upstream
|
|
408
|
+
* (smartmemory/managed/framework.py:377 — `default_confidence=0.8`), so an
|
|
409
|
+
* unrated conviction cannot be written as null. It takes the neutral value and
|
|
410
|
+
* carries `conviction_unrated: true` alongside, which is what downstream must
|
|
411
|
+
* branch on. Stated here because a 0.5 that means "nobody asked" and a 0.5 that
|
|
412
|
+
* means "middling" are different facts wearing the same number.
|
|
413
|
+
*/
|
|
414
|
+
export function applyConvictionReview(decision, verdict) {
|
|
415
|
+
const snap = decision.context_snapshot;
|
|
416
|
+
if (!snap.conviction_review_required) return decision;
|
|
417
|
+
|
|
418
|
+
if (!verdict || !REVIEW_VERDICTS.has(verdict.verdict)) {
|
|
419
|
+
// Refuse, do not warn. An unreviewed inferred conviction written at stated
|
|
420
|
+
// confidence is exactly the failure the gate exists to prevent, and a
|
|
421
|
+
// warning in a backfill log is not a gate.
|
|
422
|
+
throw new Error(
|
|
423
|
+
`judgment-decisions: ledger seq ${snap.ledger_seq} carries an inferred conviction with no owner verdict. `
|
|
424
|
+
+ 'Add one to docs/features/GOV-COMPOSE-SEAM-1/conviction-review.json before backfilling.',
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const level = snap.conviction?.level ?? null;
|
|
429
|
+
snap.conviction_review = { ...verdict, applied: true };
|
|
430
|
+
snap.conviction_review_required = false;
|
|
431
|
+
|
|
432
|
+
if (verdict.verdict === 'promoted') {
|
|
433
|
+
decision.source_type = 'explicit';
|
|
434
|
+
decision.confidence = (level && CONFIDENCE.stated[level] !== undefined)
|
|
435
|
+
? CONFIDENCE.stated[level]
|
|
436
|
+
: CONFIDENCE_UNSTATED;
|
|
437
|
+
return decision;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (verdict.verdict === 'choice_kept_strength_dropped') {
|
|
441
|
+
// The DECISION was the owner's, so the source is explicit; only the
|
|
442
|
+
// strength was guessed, so the number goes back to neutral.
|
|
443
|
+
decision.source_type = 'explicit';
|
|
444
|
+
decision.confidence = CONFIDENCE_UNSTATED;
|
|
445
|
+
snap.conviction_strength_dropped = true;
|
|
446
|
+
return decision;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// unrated
|
|
450
|
+
decision.source_type = 'inferred';
|
|
451
|
+
decision.confidence = CONFIDENCE_UNSTATED;
|
|
452
|
+
snap.conviction_unrated = true;
|
|
453
|
+
snap.enforceable_eligible = false;
|
|
454
|
+
return decision;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/** Apply a whole verdict file to a mapped ledger. Throws on the first gap. */
|
|
458
|
+
export function applyReviewFile(mapped, reviewFile) {
|
|
459
|
+
const verdicts = reviewFile?.verdicts ?? {};
|
|
460
|
+
for (const d of mapped.decisions) {
|
|
461
|
+
if (d.decision.context_snapshot.conviction_review_required) {
|
|
462
|
+
applyConvictionReview(d.decision, verdicts[String(d.seq)]);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
return mapped;
|
|
466
|
+
}
|
package/lib/judgment-gen.js
CHANGED
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
effectiveStore,
|
|
29
29
|
goalCutoverComplete,
|
|
30
30
|
} from './judgment/store/index.js';
|
|
31
|
+
import { buildSupersessionIndex } from './judgment/trace.js';
|
|
31
32
|
|
|
32
33
|
function atomicWrite(path, content) {
|
|
33
34
|
const tmp = `${path}.tmp.${process.pid}`;
|
|
@@ -58,10 +59,13 @@ function smartmemoryTeamId(cwd) {
|
|
|
58
59
|
export function loadSnapshot(cwd) {
|
|
59
60
|
const rawStore = createJudgmentStore(cwd);
|
|
60
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);
|
|
61
65
|
const positions = store.listPositionSlugs().map((slug) => ({
|
|
62
66
|
slug,
|
|
63
67
|
chain: store.readPositionChain(slug),
|
|
64
|
-
status: store.derivePositionStatus(slug),
|
|
68
|
+
status: store.derivePositionStatus(slug, supersession),
|
|
65
69
|
}));
|
|
66
70
|
return {
|
|
67
71
|
positions,
|
package/lib/judgment-writer.js
CHANGED
|
@@ -48,9 +48,12 @@ import {
|
|
|
48
48
|
effectiveStore,
|
|
49
49
|
goalCutoverComplete,
|
|
50
50
|
} from './judgment/store/index.js';
|
|
51
|
+
import { buildSupersessionIndex, tracePosition } from './judgment/trace.js';
|
|
51
52
|
import { syncManifest } from './judgment-attest.js';
|
|
52
53
|
import { regenerateProjections } from './judgment-gen.js';
|
|
53
54
|
import { getJudgmentValidator } from './judgment/schema.js';
|
|
55
|
+
import { ledgerEntryToDecision } from './judgment-decisions.js';
|
|
56
|
+
import { writeJudgmentDecision } from './judgment-decision-write.js';
|
|
54
57
|
import {
|
|
55
58
|
assertValidRecord,
|
|
56
59
|
assertGrounding,
|
|
@@ -3263,7 +3266,7 @@ export async function judgmentLedgerAppend(cwd, args, internal = {}) {
|
|
|
3263
3266
|
assertValidRecord('ledger_event', event);
|
|
3264
3267
|
return event;
|
|
3265
3268
|
},
|
|
3266
|
-
execute: (event) => {
|
|
3269
|
+
execute: async (event) => {
|
|
3267
3270
|
const store = createJudgmentStore(cwd);
|
|
3268
3271
|
|
|
3269
3272
|
let spawned = null;
|
|
@@ -3313,6 +3316,32 @@ export async function judgmentLedgerAppend(cwd, args, internal = {}) {
|
|
|
3313
3316
|
assertValidRecord('prediction', graded);
|
|
3314
3317
|
}
|
|
3315
3318
|
|
|
3319
|
+
// GOV-COMPOSE-SEAM-1 `canon-on-decisions` P2 — the SmartMemory write.
|
|
3320
|
+
//
|
|
3321
|
+
// BEFORE the local commit, deliberately. Under D1 the SmartMemory
|
|
3322
|
+
// decision is the canon and the markdown is a projection, so the canon
|
|
3323
|
+
// is written first and the projection follows. It is also the only
|
|
3324
|
+
// ordering that fails safely: a remote failure throws and nothing local
|
|
3325
|
+
// is written, whereas committing locally first would leave a decision
|
|
3326
|
+
// that exists in the projection and nowhere else.
|
|
3327
|
+
//
|
|
3328
|
+
// The seq is computed here rather than taken from `appendLedgerEvent`
|
|
3329
|
+
// because the key needs it before the append. Safe: we hold the advisory
|
|
3330
|
+
// lock, so the length cannot move underneath us. If the local commit
|
|
3331
|
+
// below then fails, a retry recomputes the SAME seq, hits the sidecar,
|
|
3332
|
+
// skips the remote write and completes the local one.
|
|
3333
|
+
//
|
|
3334
|
+
// `writeJudgmentDecision` is FAIL-CLOSED and returns null only when the
|
|
3335
|
+
// coupling is disabled. A live entry carrying an inferred conviction is
|
|
3336
|
+
// written on the inferred confidence scale and flagged
|
|
3337
|
+
// `conviction_review_required` for a later ruling — the P2.5 gate blocks
|
|
3338
|
+
// the BACKFILL, not new entries, which would otherwise be unrecordable.
|
|
3339
|
+
const nextSeq = store.readLedgerEvents().length + 1;
|
|
3340
|
+
const mapped = ledgerEntryToDecision(event, nextSeq);
|
|
3341
|
+
if (mapped) {
|
|
3342
|
+
await writeJudgmentDecision(cwd, mapped);
|
|
3343
|
+
}
|
|
3344
|
+
|
|
3316
3345
|
const undo = new UndoLog();
|
|
3317
3346
|
let seq;
|
|
3318
3347
|
commitWithProjections(cwd, undo, () => {
|
|
@@ -3345,18 +3374,43 @@ export async function judgmentLedgerAppend(cwd, args, internal = {}) {
|
|
|
3345
3374
|
* recent ledger. Small result (AUDIT-19) — titles and refs, never full
|
|
3346
3375
|
* document text. Replays pending intents first (reconciler-on-read).
|
|
3347
3376
|
*/
|
|
3377
|
+
/**
|
|
3378
|
+
* Causal ancestry for one position (COMP-JUDGMENT-PRECEDENT slice A).
|
|
3379
|
+
*
|
|
3380
|
+
* Read-only counterpart to getJudgmentState, which returns latest-only and so
|
|
3381
|
+
* cannot answer "what did this replace, and what did we believe then". Takes the
|
|
3382
|
+
* same lock + intent replay so the trace reflects committed state, not a
|
|
3383
|
+
* half-applied intent queue.
|
|
3384
|
+
*/
|
|
3385
|
+
export async function getJudgmentTrace(cwd, slug) {
|
|
3386
|
+
if (typeof slug !== 'string' || slug.trim() === '') {
|
|
3387
|
+
throw typedError('JUDGMENT_INPUT', 'get_judgment_trace: slug is required');
|
|
3388
|
+
}
|
|
3389
|
+
const release = await acquireJudgmentLock(cwd);
|
|
3390
|
+
try {
|
|
3391
|
+
await replayIntentsLocked(cwd);
|
|
3392
|
+
const store = effectiveStore(createJudgmentStore(cwd));
|
|
3393
|
+
return tracePosition(store, slug.trim());
|
|
3394
|
+
} finally {
|
|
3395
|
+
release();
|
|
3396
|
+
}
|
|
3397
|
+
}
|
|
3398
|
+
|
|
3348
3399
|
export async function getJudgmentState(cwd) {
|
|
3349
3400
|
const release = await acquireJudgmentLock(cwd);
|
|
3350
3401
|
try {
|
|
3351
3402
|
const replay = await replayIntentsLocked(cwd);
|
|
3352
3403
|
regenerateProjections(cwd);
|
|
3353
3404
|
const store = effectiveStore(createJudgmentStore(cwd));
|
|
3405
|
+
// Prebuilt index — see COMP-JUDGMENT-PRECEDENT: without it this loop is
|
|
3406
|
+
// O(n^2) over the store.
|
|
3407
|
+
const supersession = buildSupersessionIndex(store);
|
|
3354
3408
|
const positions = store.listPositionSlugs().map((slug) => {
|
|
3355
3409
|
const latest = store.latestPositionRevision(slug);
|
|
3356
3410
|
return {
|
|
3357
3411
|
slug,
|
|
3358
3412
|
ref: `${slug}#r${latest.rev}`,
|
|
3359
|
-
status: store.derivePositionStatus(slug),
|
|
3413
|
+
status: store.derivePositionStatus(slug, supersession),
|
|
3360
3414
|
conviction: latest.conviction?.level ?? null,
|
|
3361
3415
|
};
|
|
3362
3416
|
});
|
package/lib/lifecycle-modes.js
CHANGED
|
@@ -51,7 +51,7 @@ export const LIFECYCLE_MODES = {
|
|
|
51
51
|
ship: [],
|
|
52
52
|
},
|
|
53
53
|
skippable: ['prd', 'architecture', 'report'],
|
|
54
|
-
terminal: ['complete', 'killed'],
|
|
54
|
+
terminal: ['complete', 'killed', 'complete_backfilled'],
|
|
55
55
|
genesis: 'explore_design',
|
|
56
56
|
completablePhase: 'ship', // the phase that → complete (was hard-coded by name)
|
|
57
57
|
phaseArtifacts: ['design.md', 'prd.md', 'architecture.md', 'blueprint.md', 'plan.md', 'report.md'],
|
|
@@ -92,7 +92,7 @@ export const LIFECYCLE_MODES = {
|
|
|
92
92
|
ship: [],
|
|
93
93
|
},
|
|
94
94
|
skippable: [],
|
|
95
|
-
terminal: ['complete', 'killed'],
|
|
95
|
+
terminal: ['complete', 'killed', 'complete_backfilled'],
|
|
96
96
|
genesis: 'reproduce',
|
|
97
97
|
completablePhase: 'ship',
|
|
98
98
|
phaseArtifacts: [], // empty → assess uses the global default (preserves today's bug behavior)
|
|
@@ -121,7 +121,7 @@ export const LIFECYCLE_MODES = {
|
|
|
121
121
|
ship: [],
|
|
122
122
|
},
|
|
123
123
|
skippable: [],
|
|
124
|
-
terminal: ['complete', 'killed'],
|
|
124
|
+
terminal: ['complete', 'killed', 'complete_backfilled'],
|
|
125
125
|
genesis: 'explore_design',
|
|
126
126
|
completablePhase: 'ship',
|
|
127
127
|
phaseArtifacts: ['design.md', 'plan.md'],
|
|
@@ -163,7 +163,7 @@ export const LIFECYCLE_MODES = {
|
|
|
163
163
|
inconclusive: ['under_test', 'open'],
|
|
164
164
|
},
|
|
165
165
|
skippable: [],
|
|
166
|
-
terminal: ['superseded', 'dissolved', 'complete', 'killed'],
|
|
166
|
+
terminal: ['superseded', 'dissolved', 'complete', 'killed', 'complete_backfilled'],
|
|
167
167
|
genesis: 'open',
|
|
168
168
|
completablePhase: 'resolved',
|
|
169
169
|
phaseArtifacts: [],
|