@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,387 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* bin/judgment-migrate.js — ledger to SmartMemory decision migration
|
|
4
|
+
* (GOV-COMPOSE-SEAM-1 step 1 `canon-on-decisions`).
|
|
5
|
+
*
|
|
6
|
+
* Phase P1 ships `--dry-run` ONLY. It reads `docs/judgment/records/ledger.jsonl`,
|
|
7
|
+
* maps every decision-shaped entry through `lib/judgment-decisions.js`, and
|
|
8
|
+
* prints the mapping plus the value-spike numbers. It writes nothing, anywhere.
|
|
9
|
+
*
|
|
10
|
+
* The write path is P2 and the backfill is P3; both are blocked until the spike
|
|
11
|
+
* is reported against its pre-registered threshold, and the backfill is
|
|
12
|
+
* additionally blocked on the P2.5 inferred-conviction review gate (D4).
|
|
13
|
+
* Invoking this without `--dry-run` therefore refuses rather than doing
|
|
14
|
+
* something plausible.
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* node bin/judgment-migrate.js --dry-run [--json] [--review-batch]
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
21
|
+
import { join, resolve, dirname } from 'node:path';
|
|
22
|
+
|
|
23
|
+
import { mapLedger, applyReviewFile, CONFIDENCE } from '../lib/judgment-decisions.js';
|
|
24
|
+
import { createSmartmemoryClient } from '../lib/smartmemory-client.js';
|
|
25
|
+
import { getSmartmemoryConfig } from '../lib/smartmemory-config.js';
|
|
26
|
+
import { readSidecar, sidecarPath, writeJudgmentDecision } from '../lib/judgment-decision-write.js';
|
|
27
|
+
|
|
28
|
+
/** Pre-registered 2026-08-22, before any count was taken. */
|
|
29
|
+
const SPIKE_THRESHOLD_RULES = 10;
|
|
30
|
+
|
|
31
|
+
function readLedger(cwd) {
|
|
32
|
+
const path = join(cwd, 'docs', 'judgment', 'records', 'ledger.jsonl');
|
|
33
|
+
if (!existsSync(path)) {
|
|
34
|
+
throw new Error(`judgment-migrate: no ledger at ${path}`);
|
|
35
|
+
}
|
|
36
|
+
return readFileSync(path, 'utf8')
|
|
37
|
+
.split('\n')
|
|
38
|
+
.filter(Boolean)
|
|
39
|
+
.map((line, i) => {
|
|
40
|
+
try {
|
|
41
|
+
return JSON.parse(line);
|
|
42
|
+
} catch (err) {
|
|
43
|
+
throw new Error(`judgment-migrate: ledger line ${i + 1} is not JSON: ${err.message}`);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The owner's P2.5 verdicts. Absent, the dry run still reports (it writes
|
|
50
|
+
* nothing), but it says so — a report that silently skipped the gate would read
|
|
51
|
+
* exactly like one that passed it.
|
|
52
|
+
*/
|
|
53
|
+
function readReview(cwd) {
|
|
54
|
+
const path = join(cwd, 'docs', 'features', 'GOV-COMPOSE-SEAM-1', 'conviction-review.json');
|
|
55
|
+
if (!existsSync(path)) return null;
|
|
56
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function truncate(s, n) {
|
|
60
|
+
const one = (s ?? '').replace(/\s+/g, ' ').trim();
|
|
61
|
+
return one.length > n ? `${one.slice(0, n - 1)}…` : one;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function report(mapped, opts) {
|
|
65
|
+
const { decisions, skipped } = mapped;
|
|
66
|
+
|
|
67
|
+
const byKind = {};
|
|
68
|
+
for (const d of decisions) {
|
|
69
|
+
byKind[d.entry.kind] = (byKind[d.entry.kind] ?? 0) + 1;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const candidates = decisions.filter((d) => d.enforceability.verdict === 'candidate');
|
|
73
|
+
const inferred = decisions.filter((d) => d.decision.context_snapshot.conviction_review_required);
|
|
74
|
+
const reviewed = decisions.filter((d) => d.decision.context_snapshot.conviction_review);
|
|
75
|
+
|
|
76
|
+
if (opts.json) {
|
|
77
|
+
process.stdout.write(`${JSON.stringify({
|
|
78
|
+
total_entries: decisions.length + skipped.length,
|
|
79
|
+
decision_shaped: decisions.length,
|
|
80
|
+
by_kind: byKind,
|
|
81
|
+
skipped: skipped.length,
|
|
82
|
+
enforceable_candidates: candidates.length,
|
|
83
|
+
threshold: SPIKE_THRESHOLD_RULES,
|
|
84
|
+
inferred_conviction_review_required: inferred.length,
|
|
85
|
+
inferred_conviction_reviewed: reviewed.length,
|
|
86
|
+
review_ruled_at: opts.review?.ruled_at ?? null,
|
|
87
|
+
decisions: decisions.map((d) => ({
|
|
88
|
+
seq: d.seq,
|
|
89
|
+
kind: d.entry.kind,
|
|
90
|
+
decision: d.decision,
|
|
91
|
+
enforceability: d.enforceability,
|
|
92
|
+
})),
|
|
93
|
+
}, null, 2)}\n`);
|
|
94
|
+
return candidates.length;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const out = [];
|
|
98
|
+
out.push('# judgment-migrate --dry-run (GOV-COMPOSE-SEAM-1 canon-on-decisions P1)');
|
|
99
|
+
out.push('');
|
|
100
|
+
out.push('NOTHING WAS WRITTEN. This is the mapping and the spike measurement only.');
|
|
101
|
+
out.push('');
|
|
102
|
+
out.push(`Ledger entries: ${decisions.length + skipped.length}`);
|
|
103
|
+
out.push(`Decision-shaped: ${decisions.length} (${Object.entries(byKind).map(([k, v]) => `${k}=${v}`).join(' ')})`);
|
|
104
|
+
out.push(`Not decisions: ${skipped.length} (note/escalate/override/attest/calibrate — D3)`);
|
|
105
|
+
out.push('');
|
|
106
|
+
out.push('## Value spike (threshold pre-registered 2026-08-22)');
|
|
107
|
+
out.push('');
|
|
108
|
+
out.push(`Enforceable CANDIDATES: ${candidates.length} (threshold to ship consume-bundle: >= ${SPIKE_THRESHOLD_RULES})`);
|
|
109
|
+
out.push('');
|
|
110
|
+
out.push('A candidate is NOT a counted rule. The classifier fires when all three D5');
|
|
111
|
+
out.push('signals are present; the reported spike number is the ADJUDICATED count after a');
|
|
112
|
+
out.push('human confirms each candidate actually names a step, names an observable, and');
|
|
113
|
+
out.push('could be violated by a real build. Report the adjudicated number, not this one.');
|
|
114
|
+
out.push('');
|
|
115
|
+
out.push('## Enforceable candidates (adjudicate these)');
|
|
116
|
+
out.push('');
|
|
117
|
+
for (const d of candidates) {
|
|
118
|
+
const s = d.enforceability.signals;
|
|
119
|
+
out.push(`- [${d.seq}] ${d.entry.kind} / ${d.decision.decision_type} step=${s.namesStep} obs=${s.namesObservable} violable=${s.violable}`);
|
|
120
|
+
out.push(` ${truncate(d.decision.content, 100)}`);
|
|
121
|
+
}
|
|
122
|
+
out.push('');
|
|
123
|
+
out.push('## Inferred convictions (P2.5 review gate — D4)');
|
|
124
|
+
out.push('');
|
|
125
|
+
out.push(`Inferred scale: high=${CONFIDENCE.inferred.high} medium=${CONFIDENCE.inferred.medium} low=${CONFIDENCE.inferred.low}`);
|
|
126
|
+
out.push(`Stated scale: high=${CONFIDENCE.stated.high} medium=${CONFIDENCE.stated.medium} low=${CONFIDENCE.stated.low}`);
|
|
127
|
+
out.push('');
|
|
128
|
+
if (!opts.review) {
|
|
129
|
+
out.push(`${inferred.length} decision-shaped entries carry an agent-inferred conviction, and NO`);
|
|
130
|
+
out.push('verdict file was found. The backfill REFUSES these until the owner rules on each.');
|
|
131
|
+
for (const d of inferred) {
|
|
132
|
+
const c = d.decision.context_snapshot.conviction;
|
|
133
|
+
out.push(`- [${d.seq}] ${c.level} (inferred -> ${d.decision.confidence}) ${truncate(d.decision.content, 90)}`);
|
|
134
|
+
}
|
|
135
|
+
} else {
|
|
136
|
+
const byGroup = {};
|
|
137
|
+
for (const d of reviewed) {
|
|
138
|
+
const r = d.decision.context_snapshot.conviction_review;
|
|
139
|
+
byGroup[r.verdict] = (byGroup[r.verdict] ?? 0) + 1;
|
|
140
|
+
}
|
|
141
|
+
out.push(`RULED ${reviewed.length}/${reviewed.length} by ${opts.review.ruled_by} on ${opts.review.ruled_at}:`);
|
|
142
|
+
for (const [v, n] of Object.entries(byGroup)) out.push(` ${v}: ${n}`);
|
|
143
|
+
out.push('');
|
|
144
|
+
for (const d of reviewed) {
|
|
145
|
+
const snap = d.decision.context_snapshot;
|
|
146
|
+
const marks = [
|
|
147
|
+
snap.conviction_strength_dropped ? 'strength-dropped' : null,
|
|
148
|
+
snap.conviction_unrated ? 'UNRATED, not rule-eligible' : null,
|
|
149
|
+
].filter(Boolean).join(', ');
|
|
150
|
+
out.push(`- [${d.seq}] ${snap.conviction.level} ${snap.conviction_review.verdict} -> ${d.decision.confidence}/${d.decision.source_type}${marks ? ` (${marks})` : ''}`);
|
|
151
|
+
out.push(` ${truncate(d.decision.content, 90)}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (opts.reviewBatch) {
|
|
156
|
+
out.push('');
|
|
157
|
+
out.push('## Review batch (full text of each inferred entry)');
|
|
158
|
+
for (const d of (opts.review ? reviewed : inferred)) {
|
|
159
|
+
out.push('');
|
|
160
|
+
out.push(`### [${d.seq}] ${d.entry.title}`);
|
|
161
|
+
out.push(`conviction: ${d.decision.context_snapshot.conviction.level} (inferred)`);
|
|
162
|
+
out.push('');
|
|
163
|
+
out.push(d.entry.body ?? '(no body)');
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
out.push('');
|
|
168
|
+
process.stdout.write(`${out.join('\n')}\n`);
|
|
169
|
+
return candidates.length;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
// ── P3: the backfill ────────────────────────────────────────────────────────
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Resume state is the SHARED idempotency ledger in `lib/judgment-decision-write.js`
|
|
177
|
+
* (`docs/judgment/records/decision-ids.json`), NOT a private file.
|
|
178
|
+
*
|
|
179
|
+
* This backfill briefly kept its own under `.compose/data/`, keyed identically
|
|
180
|
+
* but read separately from the live write path. Same key, two ledgers, neither
|
|
181
|
+
* reading the other: a decision recorded live and then backfilled would be
|
|
182
|
+
* written twice, and the create endpoint has no server-side idempotency to
|
|
183
|
+
* catch it. That is the same "two mechanisms guarding one fact" failure D2
|
|
184
|
+
* retired the markdown hash chain over. Merged 2026-08-22; the old file is
|
|
185
|
+
* adopted on first read by `readSidecar` so a migration already run is not
|
|
186
|
+
* repeated.
|
|
187
|
+
*
|
|
188
|
+
* The per-write flush and the verify-before-skip both survive the merge — they
|
|
189
|
+
* moved INTO the shared writer, which is where the live path needed them too.
|
|
190
|
+
*/
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Resolve where the backfill writes.
|
|
194
|
+
*
|
|
195
|
+
* Flags override `.compose/compose.json#smartmemory`. Both a workspace and a
|
|
196
|
+
* key-env name are REQUIRED and there is no default: a backfill that guesses
|
|
197
|
+
* its destination can write 43 decisions into someone's working memory, and
|
|
198
|
+
* that is not recoverable by re-running it.
|
|
199
|
+
*/
|
|
200
|
+
function resolveTarget(cwd, argv) {
|
|
201
|
+
const flag = (name) => {
|
|
202
|
+
const i = argv.indexOf(name);
|
|
203
|
+
return i >= 0 ? argv[i + 1] : undefined;
|
|
204
|
+
};
|
|
205
|
+
const cfg = getSmartmemoryConfig(cwd);
|
|
206
|
+
const baseUrl = flag('--api-url') ?? cfg.baseUrl ?? process.env.SMARTMEMORY_API_URL;
|
|
207
|
+
const workspaceId = flag('--workspace') ?? cfg.workspaceId;
|
|
208
|
+
const apiKeyEnv = flag('--api-key-env') ?? cfg.apiKeyEnv;
|
|
209
|
+
|
|
210
|
+
const missing = [];
|
|
211
|
+
if (!baseUrl) missing.push('--api-url (or smartmemory.baseUrl)');
|
|
212
|
+
if (!workspaceId) missing.push('--workspace (or smartmemory.workspaceId)');
|
|
213
|
+
if (!apiKeyEnv) missing.push('--api-key-env (or smartmemory.apiKeyEnv)');
|
|
214
|
+
if (missing.length) {
|
|
215
|
+
throw new Error(
|
|
216
|
+
`judgment-migrate --apply: refusing to guess a destination. Missing ${missing.join(', ')}.`,
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
if (!process.env[apiKeyEnv]) {
|
|
220
|
+
throw new Error(`judgment-migrate --apply: $${apiKeyEnv} is not set`);
|
|
221
|
+
}
|
|
222
|
+
// `enabled: true` is the flags themselves. The config gate exists so the
|
|
223
|
+
// coupling is OFF by default for the live tool path; naming a destination
|
|
224
|
+
// explicitly on an `--apply` invocation IS the opt-in, and requiring the
|
|
225
|
+
// operator to also edit compose.json first would mean a one-off migration
|
|
226
|
+
// could not run without turning the live emitter on for every later build.
|
|
227
|
+
return { enabled: true, baseUrl, workspaceId, apiKeyEnv, timeoutMs: 15000 };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** The payload the HTTP contract accepts. Anything the mapper carries that the
|
|
231
|
+
* route has no field for rides in `context_snapshot`, never silently dropped. */
|
|
232
|
+
function toCreateBody(d) {
|
|
233
|
+
return {
|
|
234
|
+
content: d.content,
|
|
235
|
+
decision_type: d.decision_type,
|
|
236
|
+
confidence: d.confidence,
|
|
237
|
+
domain: d.domain,
|
|
238
|
+
tags: d.tags,
|
|
239
|
+
rejected_alternatives: d.rejected_alternatives,
|
|
240
|
+
rationale: d.rationale,
|
|
241
|
+
source_type: d.source_type,
|
|
242
|
+
context_snapshot: d.context_snapshot,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function backfill(cwd, mapped, target, opts) {
|
|
247
|
+
const client = createSmartmemoryClient(target);
|
|
248
|
+
const written = [];
|
|
249
|
+
const skipped = [];
|
|
250
|
+
const failed = [];
|
|
251
|
+
|
|
252
|
+
for (const row of mapped.decisions) {
|
|
253
|
+
const d = row.decision;
|
|
254
|
+
const key = d.idempotency_key;
|
|
255
|
+
try {
|
|
256
|
+
// One write path for the live tool call and the bulk migration. It owns
|
|
257
|
+
// the shared ledger, the verify-before-skip, the provenance check and the
|
|
258
|
+
// per-write flush; this loop owns only batching and reporting.
|
|
259
|
+
const res = await writeJudgmentDecision(cwd, d, { client, config: target });
|
|
260
|
+
if (res === null) {
|
|
261
|
+
failed.push({ seq: row.seq, key, stage: 'config', error: 'smartmemory coupling is disabled' });
|
|
262
|
+
if (opts.failFast) break;
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
(res.skipped ? skipped : written).push({ seq: row.seq, key, decision_id: res.decision_id });
|
|
266
|
+
} catch (err) {
|
|
267
|
+
// Fail-closed. A dropped decision silently un-governs a build, so the
|
|
268
|
+
// backfill reports and stops counting it as done rather than warning on.
|
|
269
|
+
failed.push({ seq: row.seq, key, stage: 'create', error: err.message });
|
|
270
|
+
if (opts.failFast) break;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return { written, skipped, failed, state: { written: readSidecar(cwd) } };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* The `correct` -> supersede half of D3, which this backfill CANNOT complete.
|
|
279
|
+
*
|
|
280
|
+
* `POST /memory/decisions/{id}/supersede` mints its replacement from
|
|
281
|
+
* `new_content` / `new_decision_type` / `new_confidence` alone — it accepts no
|
|
282
|
+
* `source_type`, no `context_snapshot`, no tags. Calling it after writing the
|
|
283
|
+
* 43 would produce a 44th, 45th and 46th decision with no idempotency key and
|
|
284
|
+
* no provenance, and would break the "run it twice, get 43" property.
|
|
285
|
+
*
|
|
286
|
+
* So the link is recorded and reported, not faked. Closing it needs a service
|
|
287
|
+
* change (supersede taking an existing decision id, or create taking
|
|
288
|
+
* `supersedes`), which is a separate piece of work.
|
|
289
|
+
*/
|
|
290
|
+
function pendingSupersedes(mapped) {
|
|
291
|
+
return mapped.decisions
|
|
292
|
+
.filter((r) => r.decision.supersedes_slug)
|
|
293
|
+
.map((r) => ({ seq: r.seq, slug: r.decision.supersedes_slug, content: r.decision.content }));
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function writeReport(cwd, result, mapped, target) {
|
|
297
|
+
const pend = pendingSupersedes(mapped);
|
|
298
|
+
const lines = [];
|
|
299
|
+
lines.push('# GOV-COMPOSE-SEAM-1 `canon-on-decisions` P3 — backfill report');
|
|
300
|
+
lines.push('');
|
|
301
|
+
lines.push(`**Target workspace:** \`${target.workspaceId}\` · **API:** \`${target.baseUrl}\``);
|
|
302
|
+
lines.push('');
|
|
303
|
+
lines.push(`- Decision-shaped ledger entries: **${mapped.decisions.length}**`);
|
|
304
|
+
lines.push(`- Written this run: **${result.written.length}**`);
|
|
305
|
+
lines.push(`- Already present (verified server-side, skipped): **${result.skipped.length}**`);
|
|
306
|
+
lines.push(`- Failed: **${result.failed.length}**`);
|
|
307
|
+
lines.push('');
|
|
308
|
+
if (result.failed.length) {
|
|
309
|
+
lines.push('## Failures');
|
|
310
|
+
lines.push('');
|
|
311
|
+
for (const f of result.failed) lines.push(`- [${f.seq}] ${f.stage}: ${f.error}`);
|
|
312
|
+
lines.push('');
|
|
313
|
+
}
|
|
314
|
+
lines.push('## Superseding links NOT applied');
|
|
315
|
+
lines.push('');
|
|
316
|
+
lines.push('`POST /memory/decisions/{id}/supersede` builds its replacement from `new_content`,');
|
|
317
|
+
lines.push('`new_decision_type` and `new_confidence` only — no `source_type`, no');
|
|
318
|
+
lines.push('`context_snapshot`, no tags. Using it here would mint extra decisions with no');
|
|
319
|
+
lines.push('idempotency key and break the run-it-twice property, so these links are recorded');
|
|
320
|
+
lines.push('and left unapplied rather than faked. Closing them needs a service change.');
|
|
321
|
+
lines.push('');
|
|
322
|
+
for (const p of pend) lines.push(`- [${p.seq}] supersedes \`${p.slug}\` — ${p.content}`);
|
|
323
|
+
lines.push('');
|
|
324
|
+
const out = join(cwd, 'docs', 'features', 'GOV-COMPOSE-SEAM-1', 'migration-report.md');
|
|
325
|
+
mkdirSync(dirname(out), { recursive: true });
|
|
326
|
+
writeFileSync(out, `${lines.join('\n')}\n`);
|
|
327
|
+
return out;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async function main(argv) {
|
|
331
|
+
const opts = {
|
|
332
|
+
dryRun: argv.includes('--dry-run'),
|
|
333
|
+
apply: argv.includes('--apply'),
|
|
334
|
+
json: argv.includes('--json'),
|
|
335
|
+
reviewBatch: argv.includes('--review-batch'),
|
|
336
|
+
failFast: !argv.includes('--keep-going'),
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
if (!opts.dryRun && !opts.apply) {
|
|
340
|
+
process.stderr.write(
|
|
341
|
+
'judgment-migrate: pass --dry-run (report only, writes nothing) or --apply (backfill).\n'
|
|
342
|
+
+ 'Refusing rather than doing something plausible.\n',
|
|
343
|
+
);
|
|
344
|
+
return 2;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const cwd = resolve(process.env.COMPOSE_CWD ?? process.cwd());
|
|
348
|
+
const events = readLedger(cwd);
|
|
349
|
+
const mapped = mapLedger(events);
|
|
350
|
+
|
|
351
|
+
// The P2.5 gate runs BEFORE anything else, in both modes. It throws on the
|
|
352
|
+
// first inferred conviction the owner has not ruled on, so the refusal shows
|
|
353
|
+
// up before a single write rather than halfway through a backfill.
|
|
354
|
+
opts.review = readReview(cwd);
|
|
355
|
+
if (opts.review) applyReviewFile(mapped, opts.review);
|
|
356
|
+
|
|
357
|
+
if (opts.dryRun) {
|
|
358
|
+
report(mapped, opts);
|
|
359
|
+
return 0;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// --apply
|
|
363
|
+
if (!opts.review) {
|
|
364
|
+
process.stderr.write(
|
|
365
|
+
'judgment-migrate --apply: no conviction-review.json found. The P2.5 gate has not been\n'
|
|
366
|
+
+ 'ruled, so the inferred convictions cannot be written. Refusing.\n',
|
|
367
|
+
);
|
|
368
|
+
return 2;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const target = resolveTarget(cwd, argv);
|
|
372
|
+
const result = await backfill(cwd, mapped, target, opts);
|
|
373
|
+
const reportPath = writeReport(cwd, result, mapped, target);
|
|
374
|
+
|
|
375
|
+
process.stdout.write(
|
|
376
|
+
`judgment-migrate --apply -> ${target.workspaceId}\n`
|
|
377
|
+
+ ` written: ${result.written.length} skipped(verified): ${result.skipped.length} `
|
|
378
|
+
+ `failed: ${result.failed.length}\n`
|
|
379
|
+
+ ` report: ${reportPath}\n`,
|
|
380
|
+
);
|
|
381
|
+
return result.failed.length ? 1 : 0;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
main(process.argv.slice(2)).then(
|
|
385
|
+
(code) => { process.exitCode = code; },
|
|
386
|
+
(err) => { process.stderr.write(`judgment-migrate: ${err.message}\n`); process.exitCode = 1; },
|
|
387
|
+
);
|
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
3
|
"$id": "https://forge.local/contracts/comp-obs-contract.schema.json",
|
|
4
4
|
"title": "COMP-OBS-CONTRACT — Wave 6 shared data model",
|
|
5
|
-
"version": "0.2.
|
|
5
|
+
"version": "0.2.7",
|
|
6
6
|
"_source": "docs/features/COMP-OBS-CONTRACT/schema.json",
|
|
7
7
|
"_roadmap": "Wave 6 — Situational Awareness",
|
|
8
8
|
"_changelog": {
|
|
9
|
+
"0.2.7": "COMP-LIFECYCLE-BACKFILL (2026-09-05) — DecisionEvent kind=phase_transition metadata gains three OPTIONAL fields (origin, recorded_at, confidence). additionalProperties:false is kept; required stays [from_phase, to_phase], so every 0.2.6 event still validates.",
|
|
10
|
+
"0.2.6": "COMP-POLICY-CHECK (2026-08-17) — DecisionEvent gains kind=policy_violation (pre-response policy check, emitter server/decision-event-emit.js buildPolicyViolationEvent) with a closed metadata subschema {step_id, rule, matched, suppressed, user_mode, build_id}. Additive — 0.2.5 events continue to validate. Note: BuildStreamEvent.schema_version const stays 0.2.5 (no BuildStreamEvent change in this bump).",
|
|
9
11
|
"0.2.5": "STRAT-PAR-STREAM — added BuildStreamEvent typed discriminated union (12 kinds): 6 new agent-narration kinds (agent_started, tool_use_summary, agent_relay closed + live; iteration_update, tier_result, health_event closed + reserved) and 6 legacy build-level kinds imported from compose/lib/build-stream-writer.js (capability_profile, capability_violation, step_usage, gate_tier_result, health_score, build_end — open metadata, catalog only; STRAT-PAR-STREAM-LEGACY-CLOSE will tighten). Additive — v0.2.4 specs continue to validate.",
|
|
10
12
|
"0.2.4": "COMP-OBS-DRIFT — added optional breach_started_at + breach_event_id to DriftAxis for stable threshold-event identity across WS reconnect; live emit and snapshot rehydration both compose the event id from these persisted fields.",
|
|
11
13
|
"0.2.3": "Codex review (2026-04-24) — two joinable-data fixes + SURFACE-split propagation. (a) `StatusSnapshot.drift_alerts[]` now a locally-defined subschema that mandates `breached: true`, so STATUS cannot emit non-alert axes through the alerts field. (b) gate `DecisionEvent.metadata.gate_log_entry_id` promoted to required — every gate DecisionEvent MUST carry its GateLogEntry id. `GateLogEntry.decision_event_id` remains nullable (explicit emission-failure escape hatch, reader reconciliation documented in prose). (c) Propagate 2026-04-23 SURFACE split (ROADMAP): `COMP-OBS-SURFACE` → `COMP-OBS-TIMELINE` (region ②, DecisionEvent producer+reader) + `COMP-OBS-STEPDETAIL` (step-level surface, no direct contract consumption in v1). `_consumers` updated; no field-shape changes beyond (a)+(b). DecisionEvent emitter ownership restated: BRANCH emits kind=branch, GATELOG emits kind=gate, TIMELINE adds kind=phase_transition (on lifecycle.advance) + kind=iteration (wrapping existing iteration-loop events), DRIFT emits kind=drift_threshold on breach.",
|
|
@@ -216,7 +218,7 @@
|
|
|
216
218
|
"id": { "type": "string", "format": "uuid" },
|
|
217
219
|
"feature_code": { "type": "string" },
|
|
218
220
|
"timestamp": { "type": "string", "format": "date-time" },
|
|
219
|
-
"kind": { "type": "string", "enum": ["gate", "branch", "iteration", "phase_transition", "drift_threshold"] },
|
|
221
|
+
"kind": { "type": "string", "enum": ["gate", "branch", "iteration", "phase_transition", "drift_threshold", "policy_violation"] },
|
|
220
222
|
"title": { "type": "string", "maxLength": 140 },
|
|
221
223
|
"roles": {
|
|
222
224
|
"type": "array",
|
|
@@ -264,11 +266,15 @@
|
|
|
264
266
|
},
|
|
265
267
|
{
|
|
266
268
|
"if": { "properties": { "kind": { "const": "phase_transition" } } },
|
|
267
|
-
"then": { "properties": { "metadata": { "type": "object", "required": ["from_phase", "to_phase"], "properties": { "from_phase": { "type": "string" }, "to_phase": { "type": "string" } }, "additionalProperties": false } } }
|
|
269
|
+
"then": { "properties": { "metadata": { "type": "object", "required": ["from_phase", "to_phase"], "properties": { "from_phase": { "type": "string" }, "to_phase": { "type": "string" }, "origin": { "type": "string", "enum": ["live", "backfill"], "description": "ABSENT means live; emitters pass stored provenance unchanged." }, "recorded_at": { "type": "string", "format": "date-time" }, "confidence": { "type": "number", "minimum": 0, "maximum": 1 } }, "additionalProperties": false } } }
|
|
268
270
|
},
|
|
269
271
|
{
|
|
270
272
|
"if": { "properties": { "kind": { "const": "drift_threshold" } } },
|
|
271
273
|
"then": { "properties": { "metadata": { "type": "object", "required": ["axis_id", "ratio", "threshold"], "properties": { "axis_id": { "type": "string", "enum": ["path_drift", "contract_drift", "review_debt_drift"] }, "ratio": { "type": "number" }, "threshold": { "type": "number" } }, "additionalProperties": false } } }
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
"if": { "properties": { "kind": { "const": "policy_violation" } } },
|
|
277
|
+
"then": { "properties": { "metadata": { "type": "object", "required": ["step_id", "rule", "matched", "suppressed", "user_mode"], "properties": { "step_id": { "type": "string" }, "rule": { "type": "string" }, "matched": { "type": "string" }, "suppressed": { "type": "boolean" }, "user_mode": { "type": "string", "enum": ["AUTONOMOUS", "PACED", "SKILL_GATED"] }, "build_id": { "type": ["string", "null"] } }, "additionalProperties": false } } }
|
|
272
278
|
}
|
|
273
279
|
]
|
|
274
280
|
},
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://compose.smartmemory.dev/contracts/fluid-record.schema.json",
|
|
4
|
+
"title": "fluid-record",
|
|
5
|
+
"version": "0.2.0",
|
|
6
|
+
"description": "The canonical record shape carried across the fluid-store provider seam (PROVIDER-SEAM, what-to-build 8k, owner-locked 2026-07-21). The seam is drawn at records + lifecycle events + capability discovery ONLY. Semantic machinery (recall, challenge, conviction, calibration, contradiction) is NEVER modelled here — those are provider capabilities that light up when the configured provider declares them, and a provider that lacks one lacks it visibly. Kind-generic from day one: COMP-PLAN-IDEA-UNIFY implements `idea` as the seam's pilot workload, and positions/joints/decisions adopt this same shape next without a contract change.",
|
|
7
|
+
"_source": "docs/features/COMP-PLAN-IDEA-UNIFY/design.md",
|
|
8
|
+
"_roadmap": "COMP-PLAN-IDEA-UNIFY",
|
|
9
|
+
"type": "object",
|
|
10
|
+
|
|
11
|
+
"definitions": {
|
|
12
|
+
|
|
13
|
+
"kind": {
|
|
14
|
+
"type": "string",
|
|
15
|
+
"description": "The fluid record's type. A provider declares which kinds it implements via supportedKinds(); an unsupported kind is refused loudly, never silently coerced. `cluster` is a record rather than a string label because an umbrella carries a hand-authored multi-sentence Theme paragraph: that is cluster-scoped content with nowhere to live on a member idea except duplicated onto every one of them, which is a drift generator. Members reference their cluster by handle via the record's `cluster` field.",
|
|
16
|
+
"enum": ["idea", "position", "joint", "decision", "thread", "question", "cluster"]
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
"handle": {
|
|
20
|
+
"type": "string",
|
|
21
|
+
"description": "Stable, human-facing external identifier (IDEA-7, JOINT-3). Distinct from `id` ON PURPOSE: `id` is provider-assigned and changes when the record is imported into a different provider, whereas the handle is quoted in docs, commits, and conversation (e.g. IDEA-20 is cited in the substrate ruling) and MUST survive a provider swap unchanged. Allocated monotonically per kind; a retired handle is never reused. The suffix is bounded to 9 digits: allocation reads it as a Number, and a value past the exact-integer range makes `max + 1` equal `max`, which would hand the same handle out twice.",
|
|
22
|
+
"pattern": "^[A-Z][A-Z0-9]*-[1-9][0-9]{0,8}$"
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
"status": {
|
|
26
|
+
"type": "string",
|
|
27
|
+
"description": "Canonical fluid lifecycle state. This is the source of truth for the record's state; any provider-native status field is a projection of it, never the reverse.",
|
|
28
|
+
"enum": ["new", "discussing", "promoted", "killed"]
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
"priority": {
|
|
32
|
+
"type": ["string", "null"],
|
|
33
|
+
"description": "Triage rank. null means untriaged (the ideabox renders this as an em dash).",
|
|
34
|
+
"enum": ["P0", "P1", "P2", null]
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
"effort": {
|
|
38
|
+
"type": ["string", "null"],
|
|
39
|
+
"description": "T-shirt size of the work, the X axis of the cockpit's 2x2 prioritization matrix. Null means unassigned, which is the matrix's `Unassigned` tray rather than a default size. Separate from `priority` on purpose: priority is a decision about sequencing, effort is an estimate of cost, and collapsing them loses the pair that makes a Quick Win distinguishable from a Big Bet.",
|
|
40
|
+
"enum": ["S", "M", "L", null]
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
"impact": {
|
|
44
|
+
"type": ["string", "null"],
|
|
45
|
+
"description": "Expected value of the work, the Y axis of the cockpit's 2x2 prioritization matrix. Null means unassigned. Lower-case by contract because that is what the markdown surface has always written and what `lib/ideabox.js` validates against; a casing change here would silently invalidate every existing assignment on upgrade.",
|
|
46
|
+
"enum": ["low", "medium", "high", null]
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
"link": {
|
|
50
|
+
"type": "object",
|
|
51
|
+
"description": "A typed edge to another record or to committed (git) canon. Promotion writes an edge of type `promoted_to` rather than mutating the record into a feature, so the idea to feature transition is provenance in the graph rather than a lost jump.",
|
|
52
|
+
"required": ["type", "target"],
|
|
53
|
+
"additionalProperties": false,
|
|
54
|
+
"properties": {
|
|
55
|
+
"type": {
|
|
56
|
+
"type": "string",
|
|
57
|
+
"enum": ["promoted_to", "maps_to", "informs", "blocks", "supports", "contradicts", "supersedes", "duplicate_of"]
|
|
58
|
+
},
|
|
59
|
+
"target": {
|
|
60
|
+
"type": "string",
|
|
61
|
+
"description": "Handle of another fluid record, or a committed feature code (COMP-FOO-1) when the edge crosses the crystallization boundary into git canon."
|
|
62
|
+
},
|
|
63
|
+
"note": { "type": "string" }
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
"provenance": {
|
|
68
|
+
"type": "object",
|
|
69
|
+
"description": "Stamped by the writing tool, never settable by a caller. Captured at write time, never retrofitted (what-to-build 12: provenance is never retrofitted successfully).",
|
|
70
|
+
"required": ["origin", "recorded_at"],
|
|
71
|
+
"additionalProperties": false,
|
|
72
|
+
"properties": {
|
|
73
|
+
"origin": {
|
|
74
|
+
"type": "string",
|
|
75
|
+
"description": "Which door the record came through. `import:ideabox` marks a record created by the one-time markdown import, distinguishing migrated rows from natively captured ones for the lifetime of the record.",
|
|
76
|
+
"enum": ["cli:ideabox", "ui:ideabox", "mcp:compose", "import:ideabox", "pipeline:plan"]
|
|
77
|
+
},
|
|
78
|
+
"recorded_at": { "type": "string", "format": "date-time" },
|
|
79
|
+
"author": { "type": ["string", "null"] }
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
"record": {
|
|
84
|
+
"type": "object",
|
|
85
|
+
"description": "One fluid record as it crosses the seam.",
|
|
86
|
+
"required": ["id", "handle", "kind", "title", "status", "provenance", "created_at", "updated_at"],
|
|
87
|
+
"additionalProperties": false,
|
|
88
|
+
"properties": {
|
|
89
|
+
"id": {
|
|
90
|
+
"type": "string",
|
|
91
|
+
"description": "Provider-assigned opaque identifier. Callers MUST NOT parse it or persist it as a cross-document reference — use `handle` for that."
|
|
92
|
+
},
|
|
93
|
+
"handle": { "$ref": "#/definitions/handle" },
|
|
94
|
+
"kind": { "$ref": "#/definitions/kind" },
|
|
95
|
+
"title": { "type": "string", "minLength": 1 },
|
|
96
|
+
"body": {
|
|
97
|
+
"type": "string",
|
|
98
|
+
"description": "Free prose. The ideabox's `Idea:` paragraph lands here."
|
|
99
|
+
},
|
|
100
|
+
"status": { "$ref": "#/definitions/status" },
|
|
101
|
+
"status_label": {
|
|
102
|
+
"type": ["string", "null"],
|
|
103
|
+
"description": "The human-facing status token when it carries more than the canonical enum can (`RE-AIMED (2026-07-21)`, `PROMOTED (-> FEAT-1)`). Canonical `status` drives all behavior; this only preserves what the author wrote so a closed enum does not quietly flatten it away. Null when the author's token was already canonical."
|
|
104
|
+
},
|
|
105
|
+
"priority": { "$ref": "#/definitions/priority" },
|
|
106
|
+
"effort": { "$ref": "#/definitions/effort" },
|
|
107
|
+
"impact": { "$ref": "#/definitions/impact" },
|
|
108
|
+
"cluster": {
|
|
109
|
+
"type": ["string", "null"],
|
|
110
|
+
"description": "HANDLE of the `cluster` record this record belongs to (e.g. CLUS-1), or null when ungrouped. Carries the editorial grouping the ideabox renders as `### Umbrella A - Resilience: fail loud, recover fast`. This is RECORD DATA, not presentation: the headings and their theme prose are hand-authored information, and a projection that cannot reproduce them from the store would clobber them on first regeneration - the failure mode `roadmap generate` already has. A handle rather than the display name so renaming an umbrella does not orphan its members."
|
|
111
|
+
},
|
|
112
|
+
"cluster_order": {
|
|
113
|
+
"type": ["integer", "null"],
|
|
114
|
+
"description": "Position of this record's cluster in the rendered projection, preserving hand-authored cluster ORDERING (Umbrella A before B) across a regenerate. Null when ungrouped."
|
|
115
|
+
},
|
|
116
|
+
"tags": {
|
|
117
|
+
"type": "array",
|
|
118
|
+
"items": { "type": "string" },
|
|
119
|
+
"description": "Bare tag tokens without the leading hash (ux, core, distribution)."
|
|
120
|
+
},
|
|
121
|
+
"source": {
|
|
122
|
+
"type": ["string", "null"],
|
|
123
|
+
"description": "Where the idea came from, as free text."
|
|
124
|
+
},
|
|
125
|
+
"links": {
|
|
126
|
+
"type": "array",
|
|
127
|
+
"items": { "$ref": "#/definitions/link" }
|
|
128
|
+
},
|
|
129
|
+
"killed": {
|
|
130
|
+
"type": ["object", "null"],
|
|
131
|
+
"description": "Set when status is `killed`. A kill is a dated event with a reason (what-to-build 11), so the reason is structured rather than appended to prose.",
|
|
132
|
+
"required": ["at", "reason"],
|
|
133
|
+
"additionalProperties": false,
|
|
134
|
+
"properties": {
|
|
135
|
+
"at": { "type": "string", "format": "date-time" },
|
|
136
|
+
"reason": { "type": "string" }
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
"discussion": {
|
|
140
|
+
"type": "array",
|
|
141
|
+
"description": "Append-only comment trail (`compose ideabox discuss`). Append-only because a deliberation trail that can be rewritten is not evidence.",
|
|
142
|
+
"items": {
|
|
143
|
+
"type": "object",
|
|
144
|
+
"required": ["at", "text"],
|
|
145
|
+
"additionalProperties": false,
|
|
146
|
+
"properties": {
|
|
147
|
+
"at": { "type": "string", "format": "date-time" },
|
|
148
|
+
"text": { "type": "string" },
|
|
149
|
+
"author": {
|
|
150
|
+
"type": ["string", "null"],
|
|
151
|
+
"description": "Who wrote the entry, or null when unattributed. Spaces ARE allowed — `Jane Doe` is the common case, and the markdown parser used to require `\\w+`, which silently dropped the whole entry (COMP-FLUID-SEAM-GUARANTEES F7-1). A colon and line breaks are forbidden: the projection's grammar is `- [date] author: text` and takes everything up to the FIRST colon as the author, so an author containing one could not be represented and would parse back as something else. Constrained here rather than escaped in the renderer, because a value the surface cannot express is a value the store should refuse to hold.",
|
|
152
|
+
"pattern": "^[^:\\n\\r]*$"
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
"provenance": { "$ref": "#/definitions/provenance" },
|
|
158
|
+
"created_at": { "type": "string", "format": "date-time" },
|
|
159
|
+
"updated_at": { "type": "string", "format": "date-time" }
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
"lifecycle_event": {
|
|
164
|
+
"type": "object",
|
|
165
|
+
"description": "An append-only fact about a record's life. Events are part of the seam because they are portable across providers; the SEMANTIC readings of them (conviction, calibration) are not, and are never derived here.",
|
|
166
|
+
"required": ["handle", "type", "at"],
|
|
167
|
+
"additionalProperties": false,
|
|
168
|
+
"properties": {
|
|
169
|
+
"handle": { "$ref": "#/definitions/handle" },
|
|
170
|
+
"type": {
|
|
171
|
+
"type": "string",
|
|
172
|
+
"enum": ["created", "updated", "triaged", "discussed", "promoted", "killed", "linked", "imported", "deleted"]
|
|
173
|
+
},
|
|
174
|
+
"at": { "type": "string", "format": "date-time" },
|
|
175
|
+
"detail": {
|
|
176
|
+
"type": "object",
|
|
177
|
+
"description": "Type-specific payload. Deliberately unconstrained: constraining it here would drag provider-specific semantics into the seam."
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
"capability": {
|
|
183
|
+
"type": "string",
|
|
184
|
+
"description": "STORAGE capabilities are the seam's own contract and every provider must implement them. SEMANTIC capabilities are the reason the seam exists: they are declared, never abstracted. Asking a provider for one it has not declared is an error (FluidCapabilityUnavailable), never an empty result - an empty result is indistinguishable from a real answer and would fake the capability.",
|
|
185
|
+
"enum": [
|
|
186
|
+
"RECORDS",
|
|
187
|
+
"EVENTS",
|
|
188
|
+
"LINKS",
|
|
189
|
+
"RECALL",
|
|
190
|
+
"CHALLENGE",
|
|
191
|
+
"CONVICTION",
|
|
192
|
+
"CALIBRATION",
|
|
193
|
+
"CONTRADICTION"
|
|
194
|
+
]
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
"additionalProperties": false,
|
|
199
|
+
"properties": {
|
|
200
|
+
"record": { "$ref": "#/definitions/record" },
|
|
201
|
+
"lifecycle_event": { "$ref": "#/definitions/lifecycle_event" }
|
|
202
|
+
},
|
|
203
|
+
"oneOf": [
|
|
204
|
+
{ "required": ["record"] },
|
|
205
|
+
{ "required": ["lifecycle_event"] }
|
|
206
|
+
],
|
|
207
|
+
|
|
208
|
+
"_validation": "Callers validate against a NAMED DEFINITION (#/definitions/record, #/definitions/lifecycle_event) — see lib/fluid/schema.js — following contracts/judgment-record.schema.json's per-definition pattern. The root is an envelope, and `oneOf` + `additionalProperties: false` exist so that an empty or unrelated payload is REJECTED by the machine rather than merely discouraged by this prose. A prose instruction with no machine effect is not a constraint."
|
|
209
|
+
}
|