@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
|
@@ -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
|
+
}
|