@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,684 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/fluid/provider.js — the fluid-store provider seam.
|
|
3
|
+
*
|
|
4
|
+
* Owner ruling: `PROVIDER-SEAM` (docs/product/2026-07-20-what-to-build-vision.md
|
|
5
|
+
* §8k, owner-locked 2026-07-21). Canonical fluid records (ideas, positions,
|
|
6
|
+
* joints, decisions, ledger entries) live behind THIS interface. The floor is a
|
|
7
|
+
* zero-install local provider; SmartMemory is the reference, capability-rich
|
|
8
|
+
* provider.
|
|
9
|
+
*
|
|
10
|
+
* The interface is drawn at exactly three things:
|
|
11
|
+
* 1. typed record CRUD
|
|
12
|
+
* 2. lifecycle events
|
|
13
|
+
* 3. capability discovery
|
|
14
|
+
*
|
|
15
|
+
* and nothing else. Semantic machinery — recall, challenge, conviction/decay,
|
|
16
|
+
* calibration, contradiction — is deliberately NOT part of the interface. Those
|
|
17
|
+
* are capabilities that light up when the configured provider declares them.
|
|
18
|
+
*
|
|
19
|
+
* WHY THIS IS THE WHOLE POINT: abstracting semantics into the seam is the
|
|
20
|
+
* lowest-common-denominator failure, and the ruling prohibits it. A provider
|
|
21
|
+
* without a capability must LACK it visibly; nothing may fake it. Concretely,
|
|
22
|
+
* `recall()` on a provider that never declared RECALL must throw — it must not
|
|
23
|
+
* return `[]`. An empty array is indistinguishable from a real "nothing matched"
|
|
24
|
+
* answer, so returning one silently converts a missing capability into a wrong
|
|
25
|
+
* answer, which is the precise failure this seam exists to prevent.
|
|
26
|
+
*
|
|
27
|
+
* Contract: contracts/fluid-record.schema.json
|
|
28
|
+
* Pattern precedent: lib/tracker/provider.js (the tracker seam).
|
|
29
|
+
* Pattern REJECTED: lib/tracker/factory.js `withFallback` — see lib/fluid/factory.js.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Capabilities every provider must implement. These ARE the seam's contract.
|
|
34
|
+
*/
|
|
35
|
+
export const STORAGE_CAP = Object.freeze({
|
|
36
|
+
RECORDS: 'RECORDS',
|
|
37
|
+
EVENTS: 'EVENTS',
|
|
38
|
+
LINKS: 'LINKS',
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Capabilities a provider may declare. These are the reason the seam exists.
|
|
43
|
+
* Declared, never abstracted — the floor has none of them, and that is correct.
|
|
44
|
+
*/
|
|
45
|
+
export const SEMANTIC_CAP = Object.freeze({
|
|
46
|
+
RECALL: 'RECALL',
|
|
47
|
+
CHALLENGE: 'CHALLENGE',
|
|
48
|
+
CONVICTION: 'CONVICTION',
|
|
49
|
+
CALIBRATION: 'CALIBRATION',
|
|
50
|
+
CONTRADICTION: 'CONTRADICTION',
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
export const CAP = Object.freeze({ ...STORAGE_CAP, ...SEMANTIC_CAP });
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Bounds for `recall(query, {limit})`. On the seam, not on a provider, so two
|
|
57
|
+
* providers cannot disagree about what `limit: 0` or `limit: 5000` means.
|
|
58
|
+
*
|
|
59
|
+
* The clamp is load-bearing rather than decorative: a provider backed by an HTTP
|
|
60
|
+
* search API typically forwards this as a result count, and an unbounded value
|
|
61
|
+
* becomes an unbounded fetch plus a proportional ranking cost on the server.
|
|
62
|
+
*/
|
|
63
|
+
export const RECALL_LIMIT_DEFAULT = 10;
|
|
64
|
+
export const RECALL_LIMIT_MIN = 1;
|
|
65
|
+
export const RECALL_LIMIT_MAX = 100;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Normalize a caller's `limit` to the seam's bounds. Never throws — recall is a
|
|
69
|
+
* discovery call, and answering sanely beats failing on a sloppy argument.
|
|
70
|
+
*
|
|
71
|
+
* The two ends are deliberately NOT symmetric:
|
|
72
|
+
*
|
|
73
|
+
* - **Too large is clamped.** `limit: 5000` is a legible intent — "give me
|
|
74
|
+
* lots" — so it is honoured up to the ceiling.
|
|
75
|
+
* - **Zero, negative and unusable take the DEFAULT, not the floor.** These carry
|
|
76
|
+
* no intent at all; they are almost always an uninitialised variable or a
|
|
77
|
+
* failed parse. Clamping `0` to `1` would answer a question nobody asked, and
|
|
78
|
+
* would do it silently — a single near-useless result looks like a real answer.
|
|
79
|
+
*
|
|
80
|
+
* @param {unknown} limit
|
|
81
|
+
* @returns {number}
|
|
82
|
+
*/
|
|
83
|
+
export function normalizeRecallLimit(limit) {
|
|
84
|
+
const n = typeof limit === 'number' && Number.isFinite(limit) ? Math.floor(limit) : NaN;
|
|
85
|
+
if (Number.isNaN(n) || n < RECALL_LIMIT_MIN) return RECALL_LIMIT_DEFAULT;
|
|
86
|
+
return Math.min(n, RECALL_LIMIT_MAX);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* One recall result.
|
|
91
|
+
*
|
|
92
|
+
* @typedef {object} RecallHit
|
|
93
|
+
* @property {string} handle
|
|
94
|
+
* The record's citation — and **the authority**. `record` below is a
|
|
95
|
+
* convenience payload; anything about to ACT on a result must re-read it with
|
|
96
|
+
* `getRecord(handle)`.
|
|
97
|
+
* @property {number|null} score
|
|
98
|
+
* The provider's relevance score, passed through untouched. `null` when the
|
|
99
|
+
* provider did not supply one — deliberately not `0`, which would sort as a
|
|
100
|
+
* real and terrible score rather than as "unknown".
|
|
101
|
+
* @property {object} record
|
|
102
|
+
* A hit-time snapshot of the record, valid against
|
|
103
|
+
* `#/definitions/record`. It is a SIBLING of `score`, never a carrier for it:
|
|
104
|
+
* the record contract is `additionalProperties: false`, so a score attached to
|
|
105
|
+
* the record object would produce something the contract rejects.
|
|
106
|
+
*
|
|
107
|
+
* **Current, not "as indexed".** A provider whose index lags its store may
|
|
108
|
+
* match on stale text while returning the current record. What can be stale is
|
|
109
|
+
* *why a hit surfaced and where it ranked* — never what you receive.
|
|
110
|
+
*/
|
|
111
|
+
|
|
112
|
+
const SEMANTIC_CAP_VALUES = Object.freeze(new Set(Object.values(SEMANTIC_CAP)));
|
|
113
|
+
const STORAGE_CAP_VALUES = Object.freeze(new Set(Object.values(STORAGE_CAP)));
|
|
114
|
+
|
|
115
|
+
export function isSemanticCapability(cap) {
|
|
116
|
+
return SEMANTIC_CAP_VALUES.has(cap);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function isStorageCapability(cap) {
|
|
120
|
+
return STORAGE_CAP_VALUES.has(cap);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* How far a provider's mutation serialization actually reaches.
|
|
125
|
+
*
|
|
126
|
+
* Declared rather than assumed, because "is this store safe for two writers" has
|
|
127
|
+
* no single answer — it depends on the mechanism AND on how far the store is
|
|
128
|
+
* reachable. A filesystem mutex genuinely serializes every process on one
|
|
129
|
+
* machine and cannot serialize two machines; saying "locked" would be true and
|
|
130
|
+
* misleading in the same breath.
|
|
131
|
+
*
|
|
132
|
+
* Ordered weakest to strongest, and comparable via {@link mutationScopeAtLeast}.
|
|
133
|
+
*/
|
|
134
|
+
export const MUTATION_SCOPE = Object.freeze({
|
|
135
|
+
/** Nothing is serialized. Concurrent writers can destroy each other's records. */
|
|
136
|
+
NONE: 'none',
|
|
137
|
+
/** Serialized within one process only — no help against a second CLI or server. */
|
|
138
|
+
PROCESS: 'process',
|
|
139
|
+
/** Serialized across every process on one machine. The floor (`lib/dir-lock.js`). */
|
|
140
|
+
MACHINE: 'machine',
|
|
141
|
+
/** Serialized across every machine reaching the store. Required of a shared store. */
|
|
142
|
+
CLUSTER: 'cluster',
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const MUTATION_SCOPE_RANK = Object.freeze({
|
|
146
|
+
[MUTATION_SCOPE.NONE]: 0,
|
|
147
|
+
[MUTATION_SCOPE.PROCESS]: 1,
|
|
148
|
+
[MUTATION_SCOPE.MACHINE]: 2,
|
|
149
|
+
[MUTATION_SCOPE.CLUSTER]: 3,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
/** True when `scope` is at least as strong as `required`. */
|
|
153
|
+
export function mutationScopeAtLeast(scope, required) {
|
|
154
|
+
return (MUTATION_SCOPE_RANK[scope] ?? -1) >= (MUTATION_SCOPE_RANK[required] ?? Infinity);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Record kinds the contract defines. A provider implements a subset. */
|
|
158
|
+
export const KIND = Object.freeze({
|
|
159
|
+
IDEA: 'idea',
|
|
160
|
+
POSITION: 'position',
|
|
161
|
+
JOINT: 'joint',
|
|
162
|
+
DECISION: 'decision',
|
|
163
|
+
THREAD: 'thread',
|
|
164
|
+
QUESTION: 'question',
|
|
165
|
+
// A grouping is a record, not a label on its members: an umbrella owns a
|
|
166
|
+
// hand-authored Theme paragraph, and the only place that could live on a
|
|
167
|
+
// member is duplicated across all of them.
|
|
168
|
+
CLUSTER: 'cluster',
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* The kinds `challenge()` accepts (COMP-FOH FOH-3). A seam-level contract, not a
|
|
173
|
+
* provider detail: only assertion-shaped kinds (a decision, an idea) can
|
|
174
|
+
* meaningfully contradict stored memory. The challenge endpoint runs its
|
|
175
|
+
* detection cascade directly with no `should_challenge` gate, so a non-assertional
|
|
176
|
+
* kind (thread/question/cluster) must be refused here rather than fed to it.
|
|
177
|
+
*/
|
|
178
|
+
export const CHALLENGEABLE_KINDS = Object.freeze(new Set([KIND.DECISION, KIND.IDEA]));
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* The resolution strategies `resolveConflict()` accepts (COMP-FOH FOH-4). A
|
|
182
|
+
* seam-level contract like {@link CHALLENGEABLE_KINDS}: v1 allows ONLY
|
|
183
|
+
* `accept_new` — the one strategy with an observable effect at this seam (it
|
|
184
|
+
* decays the target's confidence). `keep_existing` is expressed by not calling
|
|
185
|
+
* resolve at all; `keep_both`/`defer` write server-side markers nothing in
|
|
186
|
+
* Compose reads (a dead capability, refused rather than shipped); `merge` has
|
|
187
|
+
* no deterministic server effect. The parameter stays explicit so the
|
|
188
|
+
* allowlist can widen later without a signature change.
|
|
189
|
+
*
|
|
190
|
+
* A frozen ARRAY, not a frozen Set: `Object.freeze` on a Set freezes its
|
|
191
|
+
* properties but not its entries — `.add()` would still succeed, and this is
|
|
192
|
+
* the autonomy gate on a destructive write, so it must be genuinely immutable.
|
|
193
|
+
*/
|
|
194
|
+
export const CONVICTION_STRATEGIES = Object.freeze(['accept_new']);
|
|
195
|
+
|
|
196
|
+
// ---------------------------------------------------------------------------
|
|
197
|
+
// Errors
|
|
198
|
+
// ---------------------------------------------------------------------------
|
|
199
|
+
|
|
200
|
+
/** Configuration is present but wrong. Never swallowed — see factory.js. */
|
|
201
|
+
export class FluidConfigError extends Error {
|
|
202
|
+
constructor(message, detail = {}) {
|
|
203
|
+
super(message);
|
|
204
|
+
this.name = 'FluidConfigError';
|
|
205
|
+
this.detail = detail;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* The configured provider does not offer the requested capability.
|
|
211
|
+
*
|
|
212
|
+
* This is the "visibly unavailable, never faked" mechanism in code form. It
|
|
213
|
+
* names both the capability and the provider so the surface above can render a
|
|
214
|
+
* funnel ("challenge: connect SmartMemory") rather than an empty state — the
|
|
215
|
+
* distinction `COLLEAGUE-ALL-IN` turns on.
|
|
216
|
+
*/
|
|
217
|
+
export class FluidCapabilityUnavailable extends Error {
|
|
218
|
+
constructor(capability, providerName, detail = {}) {
|
|
219
|
+
super(
|
|
220
|
+
`fluid: capability ${capability} is not available on provider "${providerName}". ` +
|
|
221
|
+
`This capability is not emulated by design — connect a provider that declares it.`
|
|
222
|
+
);
|
|
223
|
+
this.name = 'FluidCapabilityUnavailable';
|
|
224
|
+
this.capability = capability;
|
|
225
|
+
this.provider = providerName;
|
|
226
|
+
this.detail = detail;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** The provider does not implement this record kind. */
|
|
231
|
+
export class FluidKindUnsupported extends Error {
|
|
232
|
+
constructor(kind, providerName, supported = []) {
|
|
233
|
+
super(
|
|
234
|
+
`fluid: record kind "${kind}" is not supported by provider "${providerName}" ` +
|
|
235
|
+
`(supported: ${supported.length ? supported.join(', ') : 'none'})`
|
|
236
|
+
);
|
|
237
|
+
this.name = 'FluidKindUnsupported';
|
|
238
|
+
this.kind = kind;
|
|
239
|
+
this.provider = providerName;
|
|
240
|
+
this.supported = supported;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* A find-or-create matched more than one record.
|
|
246
|
+
*
|
|
247
|
+
* Distinct from "not found" because the caller's options are opposite: a miss is
|
|
248
|
+
* resolved by creating, an ambiguity can only be resolved by naming a handle.
|
|
249
|
+
* Auto-picking one would silently attach work to whichever duplicate sorted
|
|
250
|
+
* first.
|
|
251
|
+
*/
|
|
252
|
+
export class FluidAmbiguousMatch extends Error {
|
|
253
|
+
constructor(kind, title, handles) {
|
|
254
|
+
super(
|
|
255
|
+
`fluid: "${title}" matches ${handles.length} ${kind} records ` +
|
|
256
|
+
`(${handles.join(', ')}). Name the handle instead.`
|
|
257
|
+
);
|
|
258
|
+
this.name = 'FluidAmbiguousMatch';
|
|
259
|
+
this.kind = kind;
|
|
260
|
+
this.title = title;
|
|
261
|
+
this.handles = handles;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** A record was addressed that does not exist. */
|
|
266
|
+
export class FluidRecordNotFound extends Error {
|
|
267
|
+
constructor(handle, providerName) {
|
|
268
|
+
super(`fluid: no record with handle "${handle}" on provider "${providerName}"`);
|
|
269
|
+
this.name = 'FluidRecordNotFound';
|
|
270
|
+
this.handle = handle;
|
|
271
|
+
this.provider = providerName;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// -- resolution failures (COMP-FOH FOH-4) -----------------------------------
|
|
276
|
+
//
|
|
277
|
+
// Four distinct outcomes of the destructive resolve path, typed because the
|
|
278
|
+
// caller's next move is different for each — and because exactly ONE of them
|
|
279
|
+
// is safe to retry. Collapsing them into a generic error would force callers
|
|
280
|
+
// back to message-matching to decide whether retrying can double-decay.
|
|
281
|
+
|
|
282
|
+
/** The caller named a resolution strategy outside the v1 allowlist. Refused
|
|
283
|
+
* before any request is made. */
|
|
284
|
+
export class FluidInvalidStrategy extends Error {
|
|
285
|
+
constructor(strategy, allowed = []) {
|
|
286
|
+
super(
|
|
287
|
+
`fluid: resolution strategy "${strategy}" is not allowed ` +
|
|
288
|
+
`(v1 allows: ${allowed.join(', ') || 'none'})`
|
|
289
|
+
);
|
|
290
|
+
this.name = 'FluidInvalidStrategy';
|
|
291
|
+
this.strategy = strategy;
|
|
292
|
+
this.allowed = allowed;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** The resolution's record pairing is invalid (e.g. target === source).
|
|
297
|
+
* A pre-mutation validation refusal — distinct from the post-mutation outcome
|
|
298
|
+
* classes below, which describe what a real attempt did. */
|
|
299
|
+
export class FluidInvalidTarget extends Error {
|
|
300
|
+
constructor(handle, reason) {
|
|
301
|
+
super(`fluid: invalid resolution target "${handle}" (${reason})`);
|
|
302
|
+
this.name = 'FluidInvalidTarget';
|
|
303
|
+
this.handle = handle;
|
|
304
|
+
this.reason = reason;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** The resolve endpoint answered cleanly but persisted nothing. The ONLY
|
|
309
|
+
* retryable resolution failure: a clean response proves the server finished
|
|
310
|
+
* and did nothing, so trying again cannot double-apply. */
|
|
311
|
+
export class FluidResolutionNoOp extends Error {
|
|
312
|
+
constructor(handle) {
|
|
313
|
+
super(`fluid: resolution of "${handle}" persisted nothing (clean no-op; safe to retry)`);
|
|
314
|
+
this.name = 'FluidResolutionNoOp';
|
|
315
|
+
this.handle = handle;
|
|
316
|
+
this.retryable = true;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** Another caller moved the target concurrently (the workspace lease covers
|
|
321
|
+
* Compose callers, not direct SmartMemory ones). NOT retryable — re-read the
|
|
322
|
+
* conviction and re-decide; the contradiction may already be resolved. */
|
|
323
|
+
export class FluidResolutionConflict extends Error {
|
|
324
|
+
constructor(handle) {
|
|
325
|
+
super(
|
|
326
|
+
`fluid: resolution of "${handle}" collided with a concurrent change — ` +
|
|
327
|
+
're-read its conviction and re-decide before resolving again'
|
|
328
|
+
);
|
|
329
|
+
this.name = 'FluidResolutionConflict';
|
|
330
|
+
this.handle = handle;
|
|
331
|
+
this.retryable = false;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** The outcome is unknowable: the transport failed in a way that does not
|
|
336
|
+
* prove the server's synchronous mutation stopped (timeout, proxy error,
|
|
337
|
+
* gateway 5xx), and reconciliation could not positively confirm the decay.
|
|
338
|
+
* Explicitly NOT retryable — the original mutation may still land, and a
|
|
339
|
+
* retry on top of it double-decays a near-irreversible value. */
|
|
340
|
+
export class FluidResolutionIndeterminate extends Error {
|
|
341
|
+
constructor(handle) {
|
|
342
|
+
super(
|
|
343
|
+
`fluid: resolution of "${handle}" has an unknowable outcome (ambiguous transport; ` +
|
|
344
|
+
'the decay may still land). Do NOT retry; re-read its conviction later.'
|
|
345
|
+
);
|
|
346
|
+
this.name = 'FluidResolutionIndeterminate';
|
|
347
|
+
this.handle = handle;
|
|
348
|
+
this.retryable = false;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const NI = (m) => { throw new Error(`FluidProvider.${m}: not implemented`); };
|
|
353
|
+
|
|
354
|
+
// ---------------------------------------------------------------------------
|
|
355
|
+
// The seam
|
|
356
|
+
// ---------------------------------------------------------------------------
|
|
357
|
+
|
|
358
|
+
export class FluidProvider {
|
|
359
|
+
/** Stable provider name, used in errors and diagnostics. */
|
|
360
|
+
name() { return NI('name'); }
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* The capabilities this provider actually offers.
|
|
364
|
+
* A provider MUST NOT declare a semantic capability it emulates.
|
|
365
|
+
* @returns {Set<string>}
|
|
366
|
+
*/
|
|
367
|
+
capabilities() { return new Set(); }
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Record kinds this provider implements.
|
|
371
|
+
* @returns {Set<string>}
|
|
372
|
+
*/
|
|
373
|
+
supportedKinds() { return new Set(); }
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* How far this provider's mutation serialization reaches.
|
|
377
|
+
*
|
|
378
|
+
* **Defaults to `NONE`, deliberately.** A provider that has not thought about
|
|
379
|
+
* concurrency has not solved it, and the default that assumes the best is the
|
|
380
|
+
* one that produced this whole feature: S3b-1's second implementation looked
|
|
381
|
+
* complete precisely because nothing ever asked it this question.
|
|
382
|
+
*
|
|
383
|
+
* @returns {string} one of {@link MUTATION_SCOPE}
|
|
384
|
+
*/
|
|
385
|
+
mutationScope() { return MUTATION_SCOPE.NONE; }
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* True when this provider's store can be reached from more than one machine.
|
|
389
|
+
*
|
|
390
|
+
* Paired with `mutationScope()` because neither answers the safety question
|
|
391
|
+
* alone: `MACHINE` scope is complete for a local directory and useless for a
|
|
392
|
+
* shared workspace. A provider that is `shared` and below `CLUSTER` scope has
|
|
393
|
+
* a real, silent data-loss hole, and that is the pair the factory warns on.
|
|
394
|
+
*/
|
|
395
|
+
isShared() { return false; }
|
|
396
|
+
|
|
397
|
+
async init(_cwd, _config) { return this; }
|
|
398
|
+
|
|
399
|
+
async health() {
|
|
400
|
+
return {
|
|
401
|
+
ok: true,
|
|
402
|
+
provider: this.name?.() ?? 'base',
|
|
403
|
+
capabilities: [...this.capabilities()],
|
|
404
|
+
kinds: [...this.supportedKinds()],
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// -- capability + kind guards ---------------------------------------------
|
|
409
|
+
|
|
410
|
+
/** True when the capability is declared. Callers use this to decide whether
|
|
411
|
+
* to OFFER a semantic affordance at all. */
|
|
412
|
+
has(cap) { return this.capabilities().has(cap); }
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Assert a capability, or throw. Every semantic entry point calls this FIRST,
|
|
416
|
+
* before any work, so that a missing capability surfaces identically whether
|
|
417
|
+
* or not the underlying store happens to hold matching data.
|
|
418
|
+
*/
|
|
419
|
+
require(cap) {
|
|
420
|
+
if (!this.has(cap)) throw new FluidCapabilityUnavailable(cap, this.name());
|
|
421
|
+
return true;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/** Assert this provider implements a record kind, or throw. */
|
|
425
|
+
requireKind(kind) {
|
|
426
|
+
if (!this.supportedKinds().has(kind)) {
|
|
427
|
+
throw new FluidKindUnsupported(kind, this.name(), [...this.supportedKinds()]);
|
|
428
|
+
}
|
|
429
|
+
return true;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// -- records (STORAGE_CAP.RECORDS) ----------------------------------------
|
|
433
|
+
//
|
|
434
|
+
// TWO OBLIGATIONS EVERY PROVIDER OWES, STATED HERE BECAUSE THEY ARE THE SEAM'S
|
|
435
|
+
// PROMISE AND NOT A PROVIDER'S CHOICE (COMP-FLUID-SEAM-GUARANTEES).
|
|
436
|
+
//
|
|
437
|
+
// Both were built in S3b-1 and both were built in ONE provider. The second
|
|
438
|
+
// implementation then satisfied this interface completely while having
|
|
439
|
+
// neither, and nothing failed — which is the whole argument for writing them
|
|
440
|
+
// down where a third implementation must read them. `record-shape.js` holds
|
|
441
|
+
// the same line for `normalizeRecord` and `UNPATCHABLE`.
|
|
442
|
+
//
|
|
443
|
+
// 1. **SERIALIZED MUTATION.** Every mutating method is atomic with respect to
|
|
444
|
+
// concurrent callers, INCLUDING callers in other processes and — for a
|
|
445
|
+
// store reachable from more than one machine — on other machines. Two
|
|
446
|
+
// failure modes ride on this, and the first is the loud one:
|
|
447
|
+
//
|
|
448
|
+
// - Handle allocation reads the maximum issued handle and adds one.
|
|
449
|
+
// Unserialized, N concurrent creates all allocate the same handle.
|
|
450
|
+
// Measured, not theorised: 3/3 rounds collided (2026-08-05). What the
|
|
451
|
+
// collision COSTS is the store's to decide, and the two providers
|
|
452
|
+
// diverge — the floor keys its file BY HANDLE, so last-writer-wins
|
|
453
|
+
// destroys N-1 records; SmartMemory writes by `item_id`, so all N
|
|
454
|
+
// persist and N-1 are silently ORPHANED behind a handle that resolves
|
|
455
|
+
// to one of them. Collision is the invariant here; loss is not.
|
|
456
|
+
// - `updateRecord`, `appendDiscussion`, `addLink` and `removeLink` are
|
|
457
|
+
// read-modify-write against one record. Unserialized, the later write
|
|
458
|
+
// erases the earlier one.
|
|
459
|
+
//
|
|
460
|
+
// The mechanism is the provider's to choose and MUST match its reach: the
|
|
461
|
+
// floor uses a filesystem mutex (`lib/dir-lock.js`), which is correct for
|
|
462
|
+
// a local directory and definitionally wrong for a store shared across
|
|
463
|
+
// machines — being shared is the entire reason to use such a store.
|
|
464
|
+
//
|
|
465
|
+
// A provider that cannot serialize must SAY SO rather than approximate
|
|
466
|
+
// it — see `mutationScope()`. Declared, never faked, exactly as with the
|
|
467
|
+
// semantic capabilities: a silent approximation of atomicity is worse
|
|
468
|
+
// than its absence, because the caller believes it is safe.
|
|
469
|
+
//
|
|
470
|
+
// 2. **`reclaimAborted` ON `createRecord`.** Creation burns a handle before
|
|
471
|
+
// the record exists (the tombstone ordering, which is the only safe
|
|
472
|
+
// direction — see the floor's `_createRecordLocked`). A crash between the
|
|
473
|
+
// two steps therefore leaves a handle that is issued, has no record, and
|
|
474
|
+
// is permanently un-creatable.
|
|
475
|
+
//
|
|
476
|
+
// `createRecord({ handle, reclaimAborted: true })` MUST reclaim exactly
|
|
477
|
+
// that state: a handle that was issued, never became live, and was never
|
|
478
|
+
// `deleted`. It MUST NOT reclaim a handle whose record exists or was
|
|
479
|
+
// deleted — a retired handle stays retired, because handles are external
|
|
480
|
+
// citations and reissuing one repoints them at a different record.
|
|
481
|
+
//
|
|
482
|
+
// This exists so the one-time import is RESTARTABLE. A migration of a
|
|
483
|
+
// project's entire corpus that cannot be re-run after a partial failure
|
|
484
|
+
// is a migration that fails permanently on its most likely failure.
|
|
485
|
+
//
|
|
486
|
+
// `test/fluid-provider-conformance.test.js` asserts both against every
|
|
487
|
+
// provider, so a new implementation cannot satisfy this interface while
|
|
488
|
+
// missing them the way the second one did.
|
|
489
|
+
|
|
490
|
+
async getRecord(_handle) { return NI('getRecord'); }
|
|
491
|
+
async listRecords(_filter) { return NI('listRecords'); }
|
|
492
|
+
async createRecord(_input) { return NI('createRecord'); }
|
|
493
|
+
async updateRecord(_handle, _patch) { return NI('updateRecord'); }
|
|
494
|
+
async deleteRecord(_handle) { return NI('deleteRecord'); }
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Find a record of `kind` whose title matches (case-insensitively), or create
|
|
498
|
+
* it. **One operation, not two calls a caller sequences.**
|
|
499
|
+
*
|
|
500
|
+
* COMP-FLUID-SEAM-GUARANTEES F6-1. `compose ideabox add --cluster "Umbrella A"`
|
|
501
|
+
* looked the cluster up and then created it, and the per-mutation lock covers
|
|
502
|
+
* each of those but not the pair — so two concurrent adds both miss and both
|
|
503
|
+
* create, leaving two clusters with the same name and the ideas split across
|
|
504
|
+
* them. The lookup is the half that is not a mutation, which is exactly why
|
|
505
|
+
* per-mutation locking could never cover it.
|
|
506
|
+
*
|
|
507
|
+
* On the seam rather than in the caller because the fix is a critical section
|
|
508
|
+
* only the provider can open, and because every future caller of "get me the
|
|
509
|
+
* thing called X" has the same race. Its atomicity is the provider's
|
|
510
|
+
* `mutationScope()` — genuinely atomic on the floor, and on a provider
|
|
511
|
+
* declaring `NONE` no better than the two calls it replaces, which is what
|
|
512
|
+
* that declaration is for.
|
|
513
|
+
*
|
|
514
|
+
* @returns {Promise<{record: object, created: boolean}>}
|
|
515
|
+
* @throws {FluidAmbiguousMatch} when more than one record already matches.
|
|
516
|
+
*/
|
|
517
|
+
async findOrCreateRecord({ kind, title }, input = {}) {
|
|
518
|
+
// Base implementation: correct, and only as atomic as the provider is. A
|
|
519
|
+
// provider that can do better overrides this; the floor does.
|
|
520
|
+
const matches = (await this.listRecords({ kind }))
|
|
521
|
+
.filter((r) => r.title.toLowerCase() === String(title).toLowerCase());
|
|
522
|
+
if (matches.length > 1) {
|
|
523
|
+
throw new FluidAmbiguousMatch(kind, title, matches.map((m) => m.handle));
|
|
524
|
+
}
|
|
525
|
+
if (matches.length === 1) return { record: matches[0], created: false };
|
|
526
|
+
return { record: await this.createRecord({ ...input, kind, title }), created: true };
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// -- links (STORAGE_CAP.LINKS) --------------------------------------------
|
|
530
|
+
|
|
531
|
+
async addLink(_handle, _link) { return NI('addLink'); }
|
|
532
|
+
async removeLink(_handle, _link) { return NI('removeLink'); }
|
|
533
|
+
|
|
534
|
+
// -- lifecycle events (STORAGE_CAP.EVENTS) --------------------------------
|
|
535
|
+
|
|
536
|
+
async appendEvent(_event) { return NI('appendEvent'); }
|
|
537
|
+
async readEvents(_handle) { return NI('readEvents'); }
|
|
538
|
+
|
|
539
|
+
// -- semantic capabilities ------------------------------------------------
|
|
540
|
+
//
|
|
541
|
+
// These are declared here ONLY so that the failure mode is uniform and
|
|
542
|
+
// typed: the base implementation's entire job is to refuse. It is deliberate
|
|
543
|
+
// that there is no fallback, no heuristic, and no degraded path. A provider
|
|
544
|
+
// that offers one of these overrides the method AND declares the capability;
|
|
545
|
+
// the two must move together, which is what makes `has()` trustworthy.
|
|
546
|
+
//
|
|
547
|
+
// See {@link RecallHit} above `recall()` for the one return shape that IS
|
|
548
|
+
// specified here rather than left to a provider.
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Semantic recall over records. Declared here, implemented by any provider
|
|
552
|
+
* that has the machinery for it.
|
|
553
|
+
*
|
|
554
|
+
* THE SHAPE IS PART OF THE SEAM, not of whichever provider gets there first.
|
|
555
|
+
* A second provider that returned bare records, or glued a score onto one,
|
|
556
|
+
* would be a different contract wearing the same method name — the exact
|
|
557
|
+
* drift that put `normalizeRecord` and `UNPATCHABLE` in `record-shape.js`.
|
|
558
|
+
*
|
|
559
|
+
* @param {string} _query free text
|
|
560
|
+
* @param {{limit?: number}} [_opts] `limit` defaults to {@link RECALL_LIMIT_DEFAULT}
|
|
561
|
+
* and is clamped to [{@link RECALL_LIMIT_MIN}, {@link RECALL_LIMIT_MAX}]. A
|
|
562
|
+
* missing or unusable value takes the default rather than throwing: recall
|
|
563
|
+
* is a discovery call, usually driven by a UI or an agent, and failing hard
|
|
564
|
+
* on a sloppy limit is worse than answering sanely.
|
|
565
|
+
* @returns {Promise<RecallHit[]>} ranked best-first
|
|
566
|
+
* @throws {FluidCapabilityUnavailable} unless the provider declares RECALL.
|
|
567
|
+
*/
|
|
568
|
+
async recall(_query, _opts) { this.require(CAP.RECALL); return NI('recall'); }
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Contradiction detection: surface stored records that contradict the record
|
|
572
|
+
* at `handle`. THE SHAPE IS PART OF THE SEAM (like {@link RecallHit}), not of
|
|
573
|
+
* whichever provider implements it first.
|
|
574
|
+
*
|
|
575
|
+
* @typedef {object} Conflict
|
|
576
|
+
* @property {string} handle a fluid record that contradicts the challenged one
|
|
577
|
+
* @property {string} existingText the contradicting record's asserted text
|
|
578
|
+
* @property {string} conflictType e.g. direct_contradiction, temporal_conflict
|
|
579
|
+
* @property {number} confidence 0..1
|
|
580
|
+
* @property {string} explanation why it conflicts
|
|
581
|
+
* @property {string} suggestedResolution e.g. keep_existing, accept_new, merge
|
|
582
|
+
*
|
|
583
|
+
* @typedef {object} ChallengeResult
|
|
584
|
+
* @property {string} assertion the challenged record's text
|
|
585
|
+
* @property {boolean} hasConflicts DERIVED from the returned conflicts, never a
|
|
586
|
+
* provider's upstream pre-filter value
|
|
587
|
+
* @property {number} confidence DERIVED from the returned conflicts (1.0 when none)
|
|
588
|
+
* @property {Conflict[]} conflicts best-first
|
|
589
|
+
*
|
|
590
|
+
* @param {string} _handle
|
|
591
|
+
* @param {{useLlm?: boolean, timeoutMs?: number}} [_opts]
|
|
592
|
+
* @returns {Promise<ChallengeResult>}
|
|
593
|
+
* @throws {FluidCapabilityUnavailable} unless the provider declares CHALLENGE.
|
|
594
|
+
*/
|
|
595
|
+
async challenge(_handle, _opts) { this.require(CAP.CHALLENGE); return NI('challenge'); }
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* Belief-strength read: a record's current confidence and its decay history
|
|
599
|
+
* (COMP-FOH FOH-4). THE SHAPE IS PART OF THE SEAM (like {@link RecallHit}).
|
|
600
|
+
*
|
|
601
|
+
* @typedef {object} ConvictionEvent
|
|
602
|
+
* @property {string} timestamp ISO
|
|
603
|
+
* @property {number} oldConfidence
|
|
604
|
+
* @property {number} newConfidence
|
|
605
|
+
* @property {number} decayFactor
|
|
606
|
+
* @property {string} reason e.g. manual_resolution:accept_new
|
|
607
|
+
* @property {string} [conflictingFact] the contradicting text, truncated server-side
|
|
608
|
+
*
|
|
609
|
+
* @typedef {object} ConvictionResult
|
|
610
|
+
* @property {string} handle the record whose conviction this is
|
|
611
|
+
* @property {number} confidence current belief-strength 0..1 (1.0 if never touched)
|
|
612
|
+
* @property {boolean} challenged DERIVED: challengeCount > 0
|
|
613
|
+
* @property {number} challengeCount
|
|
614
|
+
* @property {string|null} lastChallengedAt DERIVED: newest history event's timestamp
|
|
615
|
+
* @property {ConvictionEvent[]} history oldest-first, capped server-side (20)
|
|
616
|
+
*
|
|
617
|
+
* @param {string} _handle
|
|
618
|
+
* @returns {Promise<ConvictionResult>}
|
|
619
|
+
* @throws {FluidCapabilityUnavailable} unless the provider declares CONVICTION.
|
|
620
|
+
*/
|
|
621
|
+
async conviction(_handle) { this.require(CAP.CONVICTION); return NI('conviction'); }
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* Gated resolution of a contradiction (COMP-FOH FOH-4): decay the TARGET
|
|
625
|
+
* record's confidence because the SOURCE record supersedes it.
|
|
626
|
+
*
|
|
627
|
+
* The first fluid method that MUTATES stored memory, so the contract is
|
|
628
|
+
* spelled out here where every provider must read it:
|
|
629
|
+
* - The strategy is always caller-supplied ({@link CONVICTION_STRATEGIES};
|
|
630
|
+
* v1: `accept_new` only). Never automatic, no default.
|
|
631
|
+
* - A 0.5 decay is near-irreversible (no fluid reinforce path exists).
|
|
632
|
+
* - Intentional repeat calls decay again (1.0 → 0.5 → 0.0) — dedupe is the
|
|
633
|
+
* caller's responsibility.
|
|
634
|
+
* - Of the typed failures, ONLY {@link FluidResolutionNoOp} is safe to
|
|
635
|
+
* retry. {@link FluidResolutionIndeterminate} means the decay may still
|
|
636
|
+
* land; retrying it can double-decay.
|
|
637
|
+
* - The provider verifies the target really is a same-kind, non-self
|
|
638
|
+
* record, but NOT that it was genuinely challenged against the source —
|
|
639
|
+
* the caller is trusted to pass a pairing obtained from a real
|
|
640
|
+
* `challenge()` result (v1 trust boundary, disclosed).
|
|
641
|
+
*
|
|
642
|
+
* @param {string} _sourceHandle the surviving record (its text becomes the contradicting fact)
|
|
643
|
+
* @param {string} _targetHandle the contradicted record whose confidence decays
|
|
644
|
+
* @param {{strategy: string, timeoutMs?: number}} [_opts]
|
|
645
|
+
* @returns {Promise<ConvictionResult>} the target's post-decay conviction
|
|
646
|
+
* @throws {FluidCapabilityUnavailable} unless the provider declares CONVICTION.
|
|
647
|
+
*/
|
|
648
|
+
async resolveConflict(_sourceHandle, _targetHandle, _opts) {
|
|
649
|
+
this.require(CAP.CONVICTION);
|
|
650
|
+
return NI('resolveConflict');
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/** @throws {FluidCapabilityUnavailable} unless the provider declares CALIBRATION. */
|
|
654
|
+
async calibration(_scope) { this.require(CAP.CALIBRATION); return NI('calibration'); }
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* The records that CONTRADICT `handle` (COMP-FOH FOH-5): the itemized,
|
|
658
|
+
* resolvable form of what {@link conviction} shows only as truncated history
|
|
659
|
+
* text. Each hit is a record whose assertion was used to decay `handle`'s
|
|
660
|
+
* confidence — an INCOMING contradiction edge from that source into `handle`.
|
|
661
|
+
*
|
|
662
|
+
* **A best-effort LOWER BOUND, not a complete set.** The edge that backs a hit
|
|
663
|
+
* is written best-effort after a resolution decays confidence; if that write
|
|
664
|
+
* was abandoned (network), the decay still happened but the link may be
|
|
665
|
+
* missing. This call can therefore under-report. The authoritative, complete
|
|
666
|
+
* record of decays is `conviction(handle).history` (as text). Repeating a
|
|
667
|
+
* resolution to "repair" a missing edge is forbidden — it decays again.
|
|
668
|
+
*
|
|
669
|
+
* `handle` on each hit is the authority: re-read with `getRecord(handle)`
|
|
670
|
+
* before acting. Hits are canonicalized — a duplicate source handle yields at
|
|
671
|
+
* most one hit, the one whose item is `getRecord`'s canonical item.
|
|
672
|
+
*
|
|
673
|
+
* @typedef {object} ContradictionHit
|
|
674
|
+
* @property {string} handle the contradicting record's citation and authority
|
|
675
|
+
* @property {string} kind the contradicting record's kind
|
|
676
|
+
* @property {object} record a current snapshot, valid against `#/definitions/record`
|
|
677
|
+
*
|
|
678
|
+
* @param {string} _handle the record whose contradictions to list
|
|
679
|
+
* @returns {Promise<ContradictionHit[]>} contradicting records, canonicalized; `[]` if none
|
|
680
|
+
* @throws {FluidRecordNotFound} if `_handle` is unknown, or the target vanished mid-read.
|
|
681
|
+
* @throws {FluidCapabilityUnavailable} unless the provider declares CONTRADICTION.
|
|
682
|
+
*/
|
|
683
|
+
async contradictions(_handle) { this.require(CAP.CONTRADICTION); return NI('contradictions'); }
|
|
684
|
+
}
|