@smartmemory/compose 0.3.7 → 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/.compose-deps.json +1 -13
- package/README.md +72 -5
- package/bin/compose.js +470 -351
- 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/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
|
@@ -11,8 +11,24 @@
|
|
|
11
11
|
* instant as the new `timestamp`. The previous entry's `exitedAt` is closed out
|
|
12
12
|
* to the new entry's `enteredAt` (legacy semantic: a phase exits when its
|
|
13
13
|
* successor begins).
|
|
14
|
+
*
|
|
15
|
+
* COMP-LIFECYCLE-BACKFILL (blueprint §4.1): the history is BITEMPORAL. `enteredAt`
|
|
16
|
+
* / `exitedAt` are VALID time (when the phase was actually in force) and
|
|
17
|
+
* `recordedAt` is TRANSACTION time (when we learned it). A live walk has the two
|
|
18
|
+
* equal, which is why `appendPhaseHistory` stamps `recordedAt: timestamp`,
|
|
19
|
+
* `origin: 'live'` and `confidence: 1.0` on every NEW write. Nothing migrates:
|
|
20
|
+
* records written before this feature carry none of the three, and every reader
|
|
21
|
+
* goes through `normaliseOrigin` so absence reads as 'live' in exactly one place.
|
|
14
22
|
*/
|
|
15
23
|
|
|
24
|
+
import { transitionsOf, terminalOf } from '../lib/lifecycle-modes.js';
|
|
25
|
+
import { buildPhaseGraph } from './lifecycle-guard.js';
|
|
26
|
+
|
|
27
|
+
/** One definition of "what does a missing `origin` mean", shared by every READER. */
|
|
28
|
+
export function normaliseOrigin(entry) {
|
|
29
|
+
return entry?.origin ?? 'live';
|
|
30
|
+
}
|
|
31
|
+
|
|
16
32
|
/**
|
|
17
33
|
* Append one phase transition entry to item.lifecycle.phaseHistory and close
|
|
18
34
|
* out the prior entry's `exitedAt`.
|
|
@@ -40,5 +56,265 @@ export function appendPhaseHistory(item, { from, to, outcome, timestamp }) {
|
|
|
40
56
|
to,
|
|
41
57
|
outcome: outcome ?? null,
|
|
42
58
|
timestamp,
|
|
59
|
+
// Bitemporal provenance (COMP-LIFECYCLE-BACKFILL). A live walk learns the
|
|
60
|
+
// transition at the instant it happens, so transaction time == valid time.
|
|
61
|
+
recordedAt: timestamp,
|
|
62
|
+
origin: 'live',
|
|
63
|
+
confidence: 1.0,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// COMP-LIFECYCLE-BACKFILL §4.1 — the valid-time merge
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
const UNIT_SEPARATOR = '\u001f';
|
|
72
|
+
|
|
73
|
+
/** BP-8: every ordering comparison is numeric on epoch ms, never on the string. */
|
|
74
|
+
const ms = (x) => Date.parse(x);
|
|
75
|
+
|
|
76
|
+
const clone = (x) => JSON.parse(JSON.stringify(x));
|
|
77
|
+
|
|
78
|
+
/** phase + UNIT SEPARATOR + evidence ref. Never a NUL byte — it breaks grep. */
|
|
79
|
+
export function occurrenceKey(o) {
|
|
80
|
+
return `${o.phase}${UNIT_SEPARATOR}${o.evidence?.ref ?? ''}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The nodes a lifecycle may actually WALK in this mode. */
|
|
84
|
+
function walkableNodes(fwdGraph) {
|
|
85
|
+
const nodes = new Set();
|
|
86
|
+
for (const [from, tos] of Object.entries(fwdGraph)) {
|
|
87
|
+
nodes.add(from);
|
|
88
|
+
for (const t of tos || []) nodes.add(t);
|
|
89
|
+
}
|
|
90
|
+
return nodes;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function reachable(graph, from, to) {
|
|
94
|
+
if (from === to) return true;
|
|
95
|
+
const seen = new Set([from]);
|
|
96
|
+
const queue = [from];
|
|
97
|
+
while (queue.length) {
|
|
98
|
+
const cur = queue.shift();
|
|
99
|
+
for (const next of graph[cur] || []) {
|
|
100
|
+
if (next === to) return true;
|
|
101
|
+
if (seen.has(next)) continue;
|
|
102
|
+
seen.add(next);
|
|
103
|
+
queue.push(next);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const IMMUTABLE_FIELDS = (x) => ({
|
|
110
|
+
phase: x.phase,
|
|
111
|
+
enteredAt: x.enteredAt,
|
|
112
|
+
timestamp: x.timestamp,
|
|
113
|
+
recordedAt: x.recordedAt,
|
|
114
|
+
outcome: x.outcome,
|
|
115
|
+
origin: x.origin,
|
|
116
|
+
evidenceKind: x.evidence?.kind ?? null,
|
|
117
|
+
evidenceRef: x.evidence?.ref ?? null,
|
|
118
|
+
observedEpochMs: x.evidence?.observedEpochMs ?? null,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const CLAIM_FIELDS = (x) => ({
|
|
122
|
+
phase: x.phase,
|
|
123
|
+
kind: x.evidence?.kind ?? null,
|
|
124
|
+
ref: x.evidence?.ref ?? null,
|
|
125
|
+
// A STORED entry carries the epoch on its evidence; an incoming one does too.
|
|
126
|
+
observedEpochMs: x.evidence?.observedEpochMs ?? null,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const eq = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
130
|
+
|
|
131
|
+
const refuse = (...reasons) => ({ ok: false, reasons: reasons.flat() });
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Merge backfilled occurrences into a phase history by VALID TIME.
|
|
135
|
+
*
|
|
136
|
+
* PURE (BP-7): `item` is never mutated and no input object is ever aliased into
|
|
137
|
+
* the result. R2B-5: the comparison baseline is an INDEPENDENT deep clone taken
|
|
138
|
+
* before anything is rewritten.
|
|
139
|
+
*
|
|
140
|
+
* @param {object} item vision item, read-only
|
|
141
|
+
* @param {object[]} incoming already materialised occurrences (§5.5)
|
|
142
|
+
* @returns {{ok:true, history:Array, written:string[], skipped:string[]}
|
|
143
|
+
* | {ok:false, reasons:string[]}}
|
|
144
|
+
*/
|
|
145
|
+
export function insertBackfilledPhases(item, incoming) {
|
|
146
|
+
const lc = item.lifecycle;
|
|
147
|
+
const mode = lc.mode ?? 'build';
|
|
148
|
+
const fwdGraph = transitionsOf(mode);
|
|
149
|
+
const fullGraph = buildPhaseGraph(mode);
|
|
150
|
+
// R2B-1: fwdGraph is what a lifecycle may WALK; fullGraph is what the guard
|
|
151
|
+
// ACCEPTS, and only fullGraph contains complete_backfilled.
|
|
152
|
+
const walkable = walkableNodes(fwdGraph);
|
|
153
|
+
const adapterTerminals = new Set(terminalOf(mode).filter((t) => !walkable.has(t)));
|
|
154
|
+
|
|
155
|
+
const startedMs = ms(lc.startedAt);
|
|
156
|
+
if (!Number.isFinite(startedMs)) {
|
|
157
|
+
return refuse('lifecycle.startedAt is not a parseable instant');
|
|
158
|
+
}
|
|
159
|
+
for (const o of incoming) {
|
|
160
|
+
if (!Number.isFinite(o.evidence?.observedEpochMs)) {
|
|
161
|
+
return refuse(`unparseable evidence time for ${occurrenceKey(o)}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
for (const e of lc.phaseHistory ?? []) {
|
|
165
|
+
if (!Number.isFinite(ms(e.enteredAt))) {
|
|
166
|
+
return refuse('stored occurrence has an unparseable enteredAt');
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// BP-7 / R2B-5: temp ids WE assign, on CLONES, plus a SEPARATE immutable
|
|
171
|
+
// snapshot cloned independently so step 4d and the marker test both read
|
|
172
|
+
// pre-rewrite values.
|
|
173
|
+
const existing = (lc.phaseHistory ?? []).map((e, seq) => ({ ...clone(e), _tid: `x${seq}`, _seq: seq }));
|
|
174
|
+
const snapshot = new Map(existing.map((e) => [e._tid, clone(e)]));
|
|
175
|
+
|
|
176
|
+
// MARKER IDENTITY IS COMPUTED NOW, off the snapshot, before any rewrite.
|
|
177
|
+
const markerTids = new Set(
|
|
178
|
+
existing
|
|
179
|
+
.filter((e) => e._seq === 0
|
|
180
|
+
&& normaliseOrigin(e) !== 'backfill'
|
|
181
|
+
&& snapshot.get(e._tid).from == null
|
|
182
|
+
&& !walkable.has(e.phase))
|
|
183
|
+
.map((e) => e._tid),
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
const skipped = [];
|
|
187
|
+
|
|
188
|
+
// ---- STEP 3a FIRST OF ALL (R3-5): dedup by operation_id.
|
|
189
|
+
const byOpId = new Map(
|
|
190
|
+
existing.filter((e) => e.operation_id != null).map((e) => [e.operation_id, e]),
|
|
191
|
+
);
|
|
192
|
+
const remaining = [];
|
|
193
|
+
for (const o of incoming) {
|
|
194
|
+
if (o.operation_id == null) { remaining.push(o); continue; }
|
|
195
|
+
const prior = byOpId.get(o.operation_id);
|
|
196
|
+
if (prior === undefined) { remaining.push(o); continue; }
|
|
197
|
+
// ALREADY PERSISTED BY THIS OPERATION. A stored entry sharing our id but
|
|
198
|
+
// differing in any immutable field is corruption, not idempotence.
|
|
199
|
+
if (!eq(IMMUTABLE_FIELDS(prior), IMMUTABLE_FIELDS(o))) {
|
|
200
|
+
return refuse(
|
|
201
|
+
`an entry under operation ${o.operation_id} is already stored with different `
|
|
202
|
+
+ 'immutable fields — refusing to overwrite it',
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
skipped.push(occurrenceKey(o));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ---- STEP 3b: dedup by CLAIM, for everything with no operation id.
|
|
209
|
+
const byKey = new Map(
|
|
210
|
+
existing.filter((e) => normaliseOrigin(e) === 'backfill').map((e) => [occurrenceKey(e), e]),
|
|
211
|
+
);
|
|
212
|
+
const batch = [];
|
|
213
|
+
for (const o of remaining) {
|
|
214
|
+
const prior = byKey.get(occurrenceKey(o));
|
|
215
|
+
if (prior === undefined) {
|
|
216
|
+
batch.push({ ...clone(o), _tid: `n${batch.length}`, _seq: Number.POSITIVE_INFINITY });
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
if (eq(CLAIM_FIELDS(prior), CLAIM_FIELDS(o))) { skipped.push(occurrenceKey(o)); continue; }
|
|
220
|
+
return refuse(`occurrence already recorded with other evidence: ${occurrenceKey(o)}`);
|
|
221
|
+
}
|
|
222
|
+
if (batch.length === 0) {
|
|
223
|
+
// Pure no-op: the persisted history is returned untouched.
|
|
224
|
+
return { ok: true, history: lc.phaseHistory ?? [], written: [], skipped };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ---- STEP 4 refusals. Whole batch, nothing persisted.
|
|
228
|
+
|
|
229
|
+
// 4a. TIE — only an INCOMING backfilled occurrence triggers it.
|
|
230
|
+
const instants = [
|
|
231
|
+
...existing.map((e) => ms(e.enteredAt)),
|
|
232
|
+
...batch.map((o) => o.evidence.observedEpochMs),
|
|
233
|
+
];
|
|
234
|
+
for (const o of batch) {
|
|
235
|
+
const at = o.evidence.observedEpochMs;
|
|
236
|
+
if (instants.filter((x) => x === at).length > 1) {
|
|
237
|
+
return refuse(`two phases cannot start at the same instant — cite distinct evidence for ${occurrenceKey(o)}`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// 4b. ADOPTION INSTANT — anchored to lc.startedAt, not to any occurrence.
|
|
242
|
+
for (const o of batch) {
|
|
243
|
+
if (o.evidence.observedEpochMs === startedMs) {
|
|
244
|
+
return refuse('backfilled occurrence at the adoption instant is ambiguously placed');
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
const episodeOf = (at) => (at < startedMs ? 1 : 2);
|
|
248
|
+
|
|
249
|
+
// 4c. CLOSED LIVE INTERVAL.
|
|
250
|
+
for (const o of batch) {
|
|
251
|
+
for (const e of existing) {
|
|
252
|
+
if (normaliseOrigin(e) === 'backfill' || e.exitedAt == null) continue;
|
|
253
|
+
const at = o.evidence.observedEpochMs;
|
|
254
|
+
if (ms(e.enteredAt) < at && at < ms(e.exitedAt)) {
|
|
255
|
+
return refuse(
|
|
256
|
+
`backfilled ${occurrenceKey(o)} falls inside the closed live interval `
|
|
257
|
+
+ `${e.phase} [${e.enteredAt}, ${e.exitedAt})`,
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// ---- STEP 2. Merge and recompute closure from valid time.
|
|
264
|
+
const instantOf = (x) => x.evidence?.observedEpochMs ?? ms(x.enteredAt);
|
|
265
|
+
const merged = [...existing, ...batch].sort((a, b) => {
|
|
266
|
+
const d = instantOf(a) - instantOf(b);
|
|
267
|
+
if (d !== 0) return d;
|
|
268
|
+
return a._seq - b._seq;
|
|
269
|
+
});
|
|
270
|
+
for (let i = 0; i < merged.length; i += 1) {
|
|
271
|
+
merged[i].exitedAt = i + 1 < merged.length ? merged[i + 1].enteredAt : null;
|
|
272
|
+
merged[i].from = i > 0 ? merged[i - 1].phase : null;
|
|
273
|
+
merged[i].episode = episodeOf(instantOf(merged[i]));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// 4d. LIVE RECORDS ARE NEVER REWRITTEN — compared against the INDEPENDENT
|
|
277
|
+
// pre-rewrite clones.
|
|
278
|
+
for (const e of existing) {
|
|
279
|
+
if (normaliseOrigin(e) === 'backfill') continue;
|
|
280
|
+
const original = snapshot.get(e._tid);
|
|
281
|
+
const m = merged.find((x) => x._tid === e._tid);
|
|
282
|
+
if (m.enteredAt !== original.enteredAt) {
|
|
283
|
+
return refuse(`the merge would move the live occurrence ${original.phase}`);
|
|
284
|
+
}
|
|
285
|
+
if (original.exitedAt != null && m.exitedAt !== original.exitedAt) {
|
|
286
|
+
return refuse(
|
|
287
|
+
`the merge would re-close the live occurrence ${original.phase} `
|
|
288
|
+
+ `from ${original.exitedAt} to ${m.exitedAt}`,
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
// An OPEN live occurrence may legitimately be closed by a later backfilled one.
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// 4e. TRANSITIVE REACHABILITY within an episode.
|
|
295
|
+
for (let i = 1; i < merged.length; i += 1) {
|
|
296
|
+
const a = merged[i - 1];
|
|
297
|
+
const b = merged[i];
|
|
298
|
+
if (markerTids.has(a._tid) || markerTids.has(b._tid)) continue;
|
|
299
|
+
if (a.episode !== b.episode) continue; // the single adoption-boundary pair
|
|
300
|
+
const g = adapterTerminals.has(b.phase) ? fullGraph : fwdGraph;
|
|
301
|
+
if (!reachable(g, a.phase, b.phase)) {
|
|
302
|
+
return refuse(`${a.phase} -> ${b.phase} is not reachable in ${mode}`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// 4f. INTERVAL INVARIANT, last, numerically, over the whole merged list.
|
|
307
|
+
for (const m of merged) {
|
|
308
|
+
if (m.exitedAt != null && ms(m.exitedAt) < ms(m.enteredAt)) {
|
|
309
|
+
return refuse(`interval invariant violated at ${m.phase}`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const history = merged.map((m) => {
|
|
314
|
+
const out = { ...m };
|
|
315
|
+
delete out._tid;
|
|
316
|
+
delete out._seq;
|
|
317
|
+
return out;
|
|
43
318
|
});
|
|
319
|
+
return { ok: true, history, written: batch.map(occurrenceKey), skipped };
|
|
44
320
|
}
|