instar 1.3.719 → 1.3.720

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.
Files changed (73) hide show
  1. package/dist/commands/server.d.ts.map +1 -1
  2. package/dist/commands/server.js +213 -31
  3. package/dist/commands/server.js.map +1 -1
  4. package/dist/core/CoherenceJournal.d.ts.map +1 -1
  5. package/dist/core/CoherenceJournal.js +9 -1
  6. package/dist/core/CoherenceJournal.js.map +1 -1
  7. package/dist/core/CoherenceJournalReader.d.ts +56 -0
  8. package/dist/core/CoherenceJournalReader.d.ts.map +1 -1
  9. package/dist/core/CoherenceJournalReader.js +148 -0
  10. package/dist/core/CoherenceJournalReader.js.map +1 -1
  11. package/dist/core/LeaseAcquisitionTrigger.d.ts +35 -0
  12. package/dist/core/LeaseAcquisitionTrigger.d.ts.map +1 -0
  13. package/dist/core/LeaseAcquisitionTrigger.js +53 -0
  14. package/dist/core/LeaseAcquisitionTrigger.js.map +1 -0
  15. package/dist/core/OwnershipReconciler.d.ts +106 -2
  16. package/dist/core/OwnershipReconciler.d.ts.map +1 -1
  17. package/dist/core/OwnershipReconciler.js +265 -12
  18. package/dist/core/OwnershipReconciler.js.map +1 -1
  19. package/dist/core/PlacementExecutor.d.ts +14 -1
  20. package/dist/core/PlacementExecutor.d.ts.map +1 -1
  21. package/dist/core/PlacementExecutor.js +17 -2
  22. package/dist/core/PlacementExecutor.js.map +1 -1
  23. package/dist/core/PostUpdateMigrator.d.ts +5 -3
  24. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  25. package/dist/core/PostUpdateMigrator.js +33 -7
  26. package/dist/core/PostUpdateMigrator.js.map +1 -1
  27. package/dist/core/SustainedOnlineTracker.d.ts +47 -0
  28. package/dist/core/SustainedOnlineTracker.d.ts.map +1 -0
  29. package/dist/core/SustainedOnlineTracker.js +67 -0
  30. package/dist/core/SustainedOnlineTracker.js.map +1 -0
  31. package/dist/core/TopicPinFoldView.d.ts +138 -0
  32. package/dist/core/TopicPinFoldView.d.ts.map +1 -0
  33. package/dist/core/TopicPinFoldView.js +235 -0
  34. package/dist/core/TopicPinFoldView.js.map +1 -0
  35. package/dist/core/TopicPinMutation.d.ts +46 -0
  36. package/dist/core/TopicPinMutation.d.ts.map +1 -0
  37. package/dist/core/TopicPinMutation.js +66 -0
  38. package/dist/core/TopicPinMutation.js.map +1 -0
  39. package/dist/core/TopicPinReplicatedStore.d.ts +3 -0
  40. package/dist/core/TopicPinReplicatedStore.d.ts.map +1 -1
  41. package/dist/core/TopicPinReplicatedStore.js +5 -0
  42. package/dist/core/TopicPinReplicatedStore.js.map +1 -1
  43. package/dist/core/TopicPinSkewQuarantine.d.ts +44 -0
  44. package/dist/core/TopicPinSkewQuarantine.d.ts.map +1 -0
  45. package/dist/core/TopicPinSkewQuarantine.js +136 -0
  46. package/dist/core/TopicPinSkewQuarantine.js.map +1 -0
  47. package/dist/core/TopicPlacementPinStore.d.ts +29 -2
  48. package/dist/core/TopicPlacementPinStore.d.ts.map +1 -1
  49. package/dist/core/TopicPlacementPinStore.js +23 -5
  50. package/dist/core/TopicPlacementPinStore.js.map +1 -1
  51. package/dist/core/types.d.ts +29 -2
  52. package/dist/core/types.d.ts.map +1 -1
  53. package/dist/core/types.js.map +1 -1
  54. package/dist/monitoring/guardManifest.d.ts.map +1 -1
  55. package/dist/monitoring/guardManifest.js +46 -0
  56. package/dist/monitoring/guardManifest.js.map +1 -1
  57. package/dist/scaffold/templates.d.ts.map +1 -1
  58. package/dist/scaffold/templates.js +1 -0
  59. package/dist/scaffold/templates.js.map +1 -1
  60. package/dist/server/AgentServer.d.ts +4 -0
  61. package/dist/server/AgentServer.d.ts.map +1 -1
  62. package/dist/server/AgentServer.js +2 -0
  63. package/dist/server/AgentServer.js.map +1 -1
  64. package/dist/server/routes.d.ts +6 -0
  65. package/dist/server/routes.d.ts.map +1 -1
  66. package/dist/server/routes.js +160 -15
  67. package/dist/server/routes.js.map +1 -1
  68. package/package.json +1 -1
  69. package/src/data/builtin-manifest.json +64 -64
  70. package/src/data/state-coherence-registry.json +18 -0
  71. package/src/scaffold/templates.ts +1 -0
  72. package/upgrades/1.3.720.md +125 -0
  73. package/upgrades/side-effects/u4-1-pin-persistence.md +49 -0
@@ -0,0 +1,47 @@
1
+ /**
2
+ * SustainedOnlineTracker — U4.1 §2E hysteresis input ("pin persistence",
3
+ * docs/specs/u4-1-pin-persistence.md).
4
+ *
5
+ * Answers ONE question for pin-driven actuation: "has this machine been
6
+ * CONTINUOUSLY online for at least the sustained window?" — the anti-flap
7
+ * gate that keeps a returning-but-flapping pinned machine from triggering
8
+ * transfer ping-pong (mirror of U4.4's hysteresis), and keeps Case-A
9
+ * cooperative transfers from initiating toward a machine that only just
10
+ * blinked online (R-r2-2: the transfer→abort churn loop).
11
+ *
12
+ * Feeding: `observe(machines)` is called on the reconciler tick cadence with
13
+ * the live pool view. A machine seen online starts (or continues) its
14
+ * online-since clock; seen offline/absent resets it.
15
+ *
16
+ * Boot honesty (deliberate fail-open): the tracker cannot distinguish
17
+ * "stable for hours" from "just flapped on" until it has watched for at
18
+ * least one full window. While its own observation history is younger than
19
+ * the queried window, `sustainedOnline` falls back to the CURRENT online
20
+ * bit (today's behavior) — otherwise every pinned placement would queue for
21
+ * the window length after every boot, a routine-latency regression the
22
+ * anti-flap gate was never meant to buy. After the tracker has ≥ window of
23
+ * history the gate is fully meaningful.
24
+ */
25
+ export interface SustainedOnlineView {
26
+ machineId: string;
27
+ online: boolean;
28
+ }
29
+ export declare class SustainedOnlineTracker {
30
+ /** machineId → epoch-ms the machine was first seen online in its CURRENT unbroken online run. */
31
+ private readonly onlineSince;
32
+ private readonly now;
33
+ /** When this tracker started observing (the boot fail-open boundary). */
34
+ private readonly trackingSince;
35
+ constructor(deps?: {
36
+ now?: () => number;
37
+ });
38
+ /** Fold one live pool view into the online-run clocks. */
39
+ observe(machines: ReadonlyArray<SustainedOnlineView>): void;
40
+ /**
41
+ * Has `machineId` been continuously online for ≥ `windowMs`?
42
+ * Boot fail-open: while the tracker's own history is younger than the
43
+ * window, this degrades to "is it online right now" (see header).
44
+ */
45
+ sustainedOnline(machineId: string, windowMs: number): boolean;
46
+ }
47
+ //# sourceMappingURL=SustainedOnlineTracker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SustainedOnlineTracker.d.ts","sourceRoot":"","sources":["../../src/core/SustainedOnlineTracker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,qBAAa,sBAAsB;IACjC,iGAAiG;IACjG,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA6B;IACzD,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IACnC,yEAAyE;IACzE,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;gBAE3B,IAAI,GAAE;QAAE,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;KAAO;IAK7C,0DAA0D;IAC1D,OAAO,CAAC,QAAQ,EAAE,aAAa,CAAC,mBAAmB,CAAC,GAAG,IAAI;IAc3D;;;;OAIG;IACH,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO;CAO9D"}
@@ -0,0 +1,67 @@
1
+ /**
2
+ * SustainedOnlineTracker — U4.1 §2E hysteresis input ("pin persistence",
3
+ * docs/specs/u4-1-pin-persistence.md).
4
+ *
5
+ * Answers ONE question for pin-driven actuation: "has this machine been
6
+ * CONTINUOUSLY online for at least the sustained window?" — the anti-flap
7
+ * gate that keeps a returning-but-flapping pinned machine from triggering
8
+ * transfer ping-pong (mirror of U4.4's hysteresis), and keeps Case-A
9
+ * cooperative transfers from initiating toward a machine that only just
10
+ * blinked online (R-r2-2: the transfer→abort churn loop).
11
+ *
12
+ * Feeding: `observe(machines)` is called on the reconciler tick cadence with
13
+ * the live pool view. A machine seen online starts (or continues) its
14
+ * online-since clock; seen offline/absent resets it.
15
+ *
16
+ * Boot honesty (deliberate fail-open): the tracker cannot distinguish
17
+ * "stable for hours" from "just flapped on" until it has watched for at
18
+ * least one full window. While its own observation history is younger than
19
+ * the queried window, `sustainedOnline` falls back to the CURRENT online
20
+ * bit (today's behavior) — otherwise every pinned placement would queue for
21
+ * the window length after every boot, a routine-latency regression the
22
+ * anti-flap gate was never meant to buy. After the tracker has ≥ window of
23
+ * history the gate is fully meaningful.
24
+ */
25
+ export class SustainedOnlineTracker {
26
+ /** machineId → epoch-ms the machine was first seen online in its CURRENT unbroken online run. */
27
+ onlineSince = new Map();
28
+ now;
29
+ /** When this tracker started observing (the boot fail-open boundary). */
30
+ trackingSince;
31
+ constructor(deps = {}) {
32
+ this.now = deps.now ?? Date.now;
33
+ this.trackingSince = this.now();
34
+ }
35
+ /** Fold one live pool view into the online-run clocks. */
36
+ observe(machines) {
37
+ const t = this.now();
38
+ const seenOnline = new Set();
39
+ for (const m of machines) {
40
+ if (!m.online)
41
+ continue;
42
+ seenOnline.add(m.machineId);
43
+ if (!this.onlineSince.has(m.machineId))
44
+ this.onlineSince.set(m.machineId, t);
45
+ }
46
+ // A machine reported offline — or absent from the view — breaks its run.
47
+ for (const id of [...this.onlineSince.keys()]) {
48
+ if (!seenOnline.has(id))
49
+ this.onlineSince.delete(id);
50
+ }
51
+ }
52
+ /**
53
+ * Has `machineId` been continuously online for ≥ `windowMs`?
54
+ * Boot fail-open: while the tracker's own history is younger than the
55
+ * window, this degrades to "is it online right now" (see header).
56
+ */
57
+ sustainedOnline(machineId, windowMs) {
58
+ const since = this.onlineSince.get(machineId);
59
+ if (since === undefined)
60
+ return false; // not online right now — never sustained
61
+ const t = this.now();
62
+ if (t - this.trackingSince < windowMs)
63
+ return true; // fail-open boot window (online now)
64
+ return t - since >= windowMs;
65
+ }
66
+ }
67
+ //# sourceMappingURL=SustainedOnlineTracker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SustainedOnlineTracker.js","sourceRoot":"","sources":["../../src/core/SustainedOnlineTracker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAOH,MAAM,OAAO,sBAAsB;IACjC,iGAAiG;IAChF,WAAW,GAAG,IAAI,GAAG,EAAkB,CAAC;IACxC,GAAG,CAAe;IACnC,yEAAyE;IACxD,aAAa,CAAS;IAEvC,YAAY,OAA+B,EAAE;QAC3C,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC;QAChC,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IAClC,CAAC;IAED,0DAA0D;IAC1D,OAAO,CAAC,QAA4C;QAClD,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACrB,MAAM,UAAU,GAAG,IAAI,GAAG,EAAU,CAAC;QACrC,KAAK,MAAM,CAAC,IAAI,QAAQ,EAAE,CAAC;YACzB,IAAI,CAAC,CAAC,CAAC,MAAM;gBAAE,SAAS;YACxB,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;YAC5B,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC;gBAAE,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC;QAC/E,CAAC;QACD,yEAAyE;QACzE,KAAK,MAAM,EAAE,IAAI,CAAC,GAAG,IAAI,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC;YAC9C,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;gBAAE,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QACvD,CAAC;IACH,CAAC;IAED;;;;OAIG;IACH,eAAe,CAAC,SAAiB,EAAE,QAAgB;QACjD,MAAM,KAAK,GAAG,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QAC9C,IAAI,KAAK,KAAK,SAAS;YAAE,OAAO,KAAK,CAAC,CAAC,yCAAyC;QAChF,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACrB,IAAI,CAAC,GAAG,IAAI,CAAC,aAAa,GAAG,QAAQ;YAAE,OAAO,IAAI,CAAC,CAAC,qCAAqC;QACzF,OAAO,CAAC,GAAG,KAAK,IAAI,QAAQ,CAAC;IAC/B,CAAC;CACF"}
@@ -0,0 +1,138 @@
1
+ /**
2
+ * TopicPinFoldView — the STATEFUL answer-complete advisory-pin read
3
+ * (U4.1 §2C, docs/specs/u4-1-pin-persistence.md; R-r2-3 / R-r2-1 / R-r3-1 /
4
+ * R-r3-2).
5
+ *
6
+ * Replaces the `query({ kind: 'topic-pin-record', limit: 2000 })` newest-tail
7
+ * read (silently clamped to 500 by `READER_MAX_LIMIT`) with:
8
+ *
9
+ * 1. A BOOT-TIME FULL-STREAM FOLD over every retained `topic-pin-record`
10
+ * entry — active file + archives, own stream AND every peer-replica
11
+ * stream — into a per-key latest map by HLC (tombstone-respecting: a
12
+ * winning delete stays in the map so a late-arriving OLDER put can never
13
+ * resurrect the pin).
14
+ * 2. An INCREMENTAL OFFSET-TRACKED TAIL after boot: each refresh re-scans
15
+ * only appended bytes (TokenLedgerPoller pattern); an unchanged journal
16
+ * costs zero re-read; re-scans are idempotent (same records → same
17
+ * winners).
18
+ * 3. The HLC SKEW GATE (fixes defect 6; R-r2-1): every record HLC passes
19
+ * through the existing `HybridLogicalClock.receive()` contract (clamped
20
+ * maxDriftMs). A future-skewed record is REJECTED FROM THE FOLD — never
21
+ * merged, never able to win `compareHlc` — and quarantined STICKILY +
22
+ * DURABLY via TopicPinSkewQuarantine (R-r3-2: the moving wall-clock
23
+ * reference must never silently un-quarantine it). The fold-side gate is
24
+ * the SOLE skew-exclusion authority (R-r3-1) — the JournalSyncApplier
25
+ * deliberately ACCEPTS-AND-PERSISTS a skewed record, because its only
26
+ * per-entry refusal path would suspect-halt the peer's ENTIRE stream.
27
+ * 4. The FOLD BYTE-GUARD (`ws13FoldMaxBytes`, R-r3-3): on breach the fold
28
+ * reads newest-first up to the budget and LOUDLY escalates (ONE deduped
29
+ * `u41:pin-fold-truncated` item naming the unfolded ranges) — never a
30
+ * silent truncation. The episode closes when a full fold completes
31
+ * within budget.
32
+ *
33
+ * Read-only over journal bytes; it actuates NOTHING (consumers are wired via
34
+ * server.ts closures exactly like the old tail read — the actuation-ban lint
35
+ * posture is unchanged).
36
+ */
37
+ import { type HlcTimestamp } from './HybridLogicalClock.js';
38
+ import { type MergedReplicatedPin } from './TopicPinReplicatedStore.js';
39
+ import type { PinFoldOffsets, PinFoldResult } from './CoherenceJournalReader.js';
40
+ import type { TopicPinSkewQuarantine } from './TopicPinSkewQuarantine.js';
41
+ import type { TopicPlacement } from './PlacementExecutor.js';
42
+ /** The reader seam (CoherenceJournalReader.foldPinRecords — injectable for tests). */
43
+ export interface PinFoldReader {
44
+ foldPinRecords(opts: {
45
+ priorOffsets?: PinFoldOffsets;
46
+ maxBytes?: number;
47
+ }): PinFoldResult;
48
+ }
49
+ export interface TopicPinFoldViewDeps {
50
+ reader: PinFoldReader;
51
+ quarantine: TopicPinSkewQuarantine;
52
+ /** This machine's mesh id (late-bound; used as the fold clock's node id). */
53
+ selfNode: () => string | null;
54
+ /** The clamped HLC drift ceiling (§2C reuses the existing knob — no new key). */
55
+ maxDriftMs?: number;
56
+ /** ws13FoldMaxBytes, read live (default 64MB upstream). */
57
+ foldMaxBytes?: () => number;
58
+ /** Optional observed pool-relative physical-time floor (heartbeat median). */
59
+ poolReference?: () => number | undefined;
60
+ now?: () => number;
61
+ /** Fired ONCE per newly-quarantined record (upstream dedupes per-origin, P17). */
62
+ onSkewQuarantined?: (rec: {
63
+ key: string;
64
+ hlc: HlcTimestamp;
65
+ origin: string;
66
+ }) => void;
67
+ /** Fired ONCE per truncation EPISODE (byte-guard breach) with the unfolded ranges. */
68
+ onFoldTruncated?: (unfolded: Array<{
69
+ file: string;
70
+ fromByte: number;
71
+ toByte: number;
72
+ }>) => void;
73
+ /** Fired when a full fold completes within budget after a breach (episode close). */
74
+ onFoldRecovered?: () => void;
75
+ log?: (msg: string) => void;
76
+ }
77
+ export declare class TopicPinFoldView {
78
+ private readonly d;
79
+ /** Per-key HLC winner (puts AND tombstones — tombstone-respecting by construction). */
80
+ private readonly winners;
81
+ private offsets;
82
+ private clock;
83
+ private truncationEpisodeOpen;
84
+ private lastFoldAtMs;
85
+ private lastScannedBytes;
86
+ private totalFolds;
87
+ constructor(deps: TopicPinFoldViewDeps);
88
+ private now;
89
+ private foldClock;
90
+ /**
91
+ * Fold newly-appended journal bytes into the winner map. The FIRST call is
92
+ * the boot-time full-stream fold (offsets start empty); later calls are the
93
+ * incremental offset-tracked tail. Never throws (a fold fault leaves the
94
+ * last-good map serving — the next refresh retries).
95
+ */
96
+ refresh(): void;
97
+ /**
98
+ * The merged advisory pins (the §2C second pass — mirrors
99
+ * `mergeUnionToPins`): a winning tombstone or `pinned:false` put yields NO
100
+ * pin; a valid winning put yields one advisory pin carrying its HLC.
101
+ */
102
+ pins(): Map<number, MergedReplicatedPin>;
103
+ /** The fold pin as placement metadata for NEW placements (§2D seeding): the
104
+ * hard-pin path handles an offline target honestly (queued, never re-routed),
105
+ * so the pin is surfaced whenever a valid winner exists. */
106
+ asTopicMetadata(sessionKey: string): TopicPlacement | undefined;
107
+ /**
108
+ * §2D placement seeding with the FULL local-vs-replicated resolution (the
109
+ * N3 rule at the placement door): the LOCAL pin and the fold winner compare
110
+ * by HLC — a stale local pin never masks a fresher replicated move-intent,
111
+ * and a fresher replicated TOMBSTONE suppresses a stale local pin (the
112
+ * defect-2 fix applied to NEW placements, not just the reconciler). A local
113
+ * pin without an HLC uses the documented `updatedAt` fallback derivation.
114
+ */
115
+ effectiveTopicMetadata(sessionKey: string, local: {
116
+ preferredMachine: string;
117
+ pinned: boolean;
118
+ updatedAt: string;
119
+ hlc?: HlcTimestamp;
120
+ } | null): TopicPlacement | undefined;
121
+ /**
122
+ * Force the NEXT refresh to be a full-stream re-fold (offsets + winners
123
+ * cleared). Used by the explicit per-record re-admission surface (R-r4-1):
124
+ * a re-admitted record's bytes were already consumed by the incremental
125
+ * tail, so only a full re-fold can bring it back into the comparison.
126
+ */
127
+ resetFold(): void;
128
+ /** Read-only observability (route/status surfaces). */
129
+ status(): {
130
+ lastFoldAt: string | null;
131
+ totalFolds: number;
132
+ lastScannedBytes: number;
133
+ recordKeys: number;
134
+ truncationEpisodeOpen: boolean;
135
+ quarantined: number;
136
+ };
137
+ }
138
+ //# sourceMappingURL=TopicPinFoldView.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TopicPinFoldView.d.ts","sourceRoot":"","sources":["../../src/core/TopicPinFoldView.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,235 @@
1
+ /**
2
+ * TopicPinFoldView — the STATEFUL answer-complete advisory-pin read
3
+ * (U4.1 §2C, docs/specs/u4-1-pin-persistence.md; R-r2-3 / R-r2-1 / R-r3-1 /
4
+ * R-r3-2).
5
+ *
6
+ * Replaces the `query({ kind: 'topic-pin-record', limit: 2000 })` newest-tail
7
+ * read (silently clamped to 500 by `READER_MAX_LIMIT`) with:
8
+ *
9
+ * 1. A BOOT-TIME FULL-STREAM FOLD over every retained `topic-pin-record`
10
+ * entry — active file + archives, own stream AND every peer-replica
11
+ * stream — into a per-key latest map by HLC (tombstone-respecting: a
12
+ * winning delete stays in the map so a late-arriving OLDER put can never
13
+ * resurrect the pin).
14
+ * 2. An INCREMENTAL OFFSET-TRACKED TAIL after boot: each refresh re-scans
15
+ * only appended bytes (TokenLedgerPoller pattern); an unchanged journal
16
+ * costs zero re-read; re-scans are idempotent (same records → same
17
+ * winners).
18
+ * 3. The HLC SKEW GATE (fixes defect 6; R-r2-1): every record HLC passes
19
+ * through the existing `HybridLogicalClock.receive()` contract (clamped
20
+ * maxDriftMs). A future-skewed record is REJECTED FROM THE FOLD — never
21
+ * merged, never able to win `compareHlc` — and quarantined STICKILY +
22
+ * DURABLY via TopicPinSkewQuarantine (R-r3-2: the moving wall-clock
23
+ * reference must never silently un-quarantine it). The fold-side gate is
24
+ * the SOLE skew-exclusion authority (R-r3-1) — the JournalSyncApplier
25
+ * deliberately ACCEPTS-AND-PERSISTS a skewed record, because its only
26
+ * per-entry refusal path would suspect-halt the peer's ENTIRE stream.
27
+ * 4. The FOLD BYTE-GUARD (`ws13FoldMaxBytes`, R-r3-3): on breach the fold
28
+ * reads newest-first up to the budget and LOUDLY escalates (ONE deduped
29
+ * `u41:pin-fold-truncated` item naming the unfolded ranges) — never a
30
+ * silent truncation. The episode closes when a full fold completes
31
+ * within budget.
32
+ *
33
+ * Read-only over journal bytes; it actuates NOTHING (consumers are wired via
34
+ * server.ts closures exactly like the old tail read — the actuation-ban lint
35
+ * posture is unchanged).
36
+ */
37
+ import { HybridLogicalClock, coerceHlc } from './HybridLogicalClock.js';
38
+ import { compareHlc, isValidPinMachineId } from './TopicPinReplicatedStore.js';
39
+ export class TopicPinFoldView {
40
+ d;
41
+ /** Per-key HLC winner (puts AND tombstones — tombstone-respecting by construction). */
42
+ winners = new Map();
43
+ offsets = {};
44
+ clock = null;
45
+ truncationEpisodeOpen = false;
46
+ lastFoldAtMs = 0;
47
+ lastScannedBytes = 0;
48
+ totalFolds = 0;
49
+ constructor(deps) {
50
+ this.d = deps;
51
+ }
52
+ now() {
53
+ return (this.d.now ?? Date.now)();
54
+ }
55
+ foldClock() {
56
+ if (!this.clock) {
57
+ this.clock = new HybridLogicalClock({
58
+ node: this.d.selfNode() ?? 'pin-fold',
59
+ now: this.d.now ?? Date.now,
60
+ ...(this.d.maxDriftMs !== undefined ? { maxDriftMs: this.d.maxDriftMs } : {}),
61
+ });
62
+ }
63
+ return this.clock;
64
+ }
65
+ /**
66
+ * Fold newly-appended journal bytes into the winner map. The FIRST call is
67
+ * the boot-time full-stream fold (offsets start empty); later calls are the
68
+ * incremental offset-tracked tail. Never throws (a fold fault leaves the
69
+ * last-good map serving — the next refresh retries).
70
+ */
71
+ refresh() {
72
+ let res;
73
+ try {
74
+ res = this.d.reader.foldPinRecords({
75
+ priorOffsets: this.offsets,
76
+ ...(this.d.foldMaxBytes ? { maxBytes: this.d.foldMaxBytes() } : {}),
77
+ });
78
+ }
79
+ catch (err) {
80
+ this.d.log?.(`[TopicPinFoldView] fold failed (serving last-good map): ${err instanceof Error ? err.message : String(err)}`);
81
+ return;
82
+ }
83
+ this.offsets = res.offsets;
84
+ this.lastFoldAtMs = this.now();
85
+ this.lastScannedBytes = res.scannedBytes;
86
+ this.totalFolds++;
87
+ const clock = this.foldClock();
88
+ for (const entry of res.entries) {
89
+ const recordKey = typeof entry.data.recordKey === 'string' ? entry.data.recordKey : null;
90
+ if (recordKey === null)
91
+ continue;
92
+ let hlc;
93
+ try {
94
+ hlc = coerceHlc(entry.data.hlc);
95
+ }
96
+ catch {
97
+ continue; // malformed hlc — schema-reject class, never a winner
98
+ }
99
+ // Sticky exclusion FIRST (R-r3-2): a quarantined (key, hlc) stays excluded
100
+ // regardless of clock progress — the moving receive() reference must never
101
+ // silently re-admit it.
102
+ if (this.d.quarantine.has(recordKey, hlc))
103
+ continue;
104
+ // The skew gate (R-r2-1): the existing receive() contract. Rejection ⇒
105
+ // durable quarantine + loud (deduped upstream) escalation; NEVER merged.
106
+ const poolReference = this.d.poolReference?.();
107
+ const received = clock.receive(hlc, poolReference !== undefined ? { poolReference } : {});
108
+ if (received.rejected) {
109
+ const newlyAdded = this.d.quarantine.add({ key: recordKey, hlc, origin: entry.origin });
110
+ if (newlyAdded) {
111
+ this.d.log?.(`[TopicPinFoldView] skew-quarantined pin record key=${recordKey} origin=${entry.origin} (physical ${hlc.physical} > reference ${received.reference} + ${received.maxDriftMs}ms)`);
112
+ try {
113
+ this.d.onSkewQuarantined?.({ key: recordKey, hlc, origin: entry.origin });
114
+ }
115
+ catch { /* escalation is observability — never gates the fold */ }
116
+ }
117
+ continue;
118
+ }
119
+ const cur = this.winners.get(recordKey);
120
+ if (!cur || compareHlc(hlc, cur.hlc) > 0) {
121
+ this.winners.set(recordKey, { data: entry.data, origin: entry.origin, hlc });
122
+ // Honest supersession (R-r3-2): a newer honest record makes older
123
+ // quarantined entries for the key dead by ordering — prune them.
124
+ try {
125
+ this.d.quarantine.pruneSuperseded(recordKey, hlc);
126
+ }
127
+ catch { /* prune is hygiene — never gates */ }
128
+ }
129
+ }
130
+ // Byte-guard episode (R-r3-3): raise ONCE per episode; close on a clean full fold.
131
+ if (res.truncated) {
132
+ if (!this.truncationEpisodeOpen) {
133
+ this.truncationEpisodeOpen = true;
134
+ try {
135
+ this.d.onFoldTruncated?.(res.unfolded);
136
+ }
137
+ catch { /* escalation never gates */ }
138
+ }
139
+ }
140
+ else if (this.truncationEpisodeOpen) {
141
+ this.truncationEpisodeOpen = false;
142
+ try {
143
+ this.d.onFoldRecovered?.();
144
+ }
145
+ catch { /* episode close is observability */ }
146
+ }
147
+ }
148
+ /**
149
+ * The merged advisory pins (the §2C second pass — mirrors
150
+ * `mergeUnionToPins`): a winning tombstone or `pinned:false` put yields NO
151
+ * pin; a valid winning put yields one advisory pin carrying its HLC.
152
+ */
153
+ pins() {
154
+ const out = new Map();
155
+ for (const [recordKey, w] of this.winners) {
156
+ if (w.data.op === 'delete')
157
+ continue;
158
+ if (w.data.pinned !== true)
159
+ continue;
160
+ const topic = Number(recordKey);
161
+ if (!Number.isFinite(topic))
162
+ continue;
163
+ if (!isValidPinMachineId(w.data.preferredMachine))
164
+ continue;
165
+ out.set(topic, { topic, preferredMachine: w.data.preferredMachine, pinned: true, origin: w.origin, hlc: w.hlc });
166
+ }
167
+ return out;
168
+ }
169
+ /** The fold pin as placement metadata for NEW placements (§2D seeding): the
170
+ * hard-pin path handles an offline target honestly (queued, never re-routed),
171
+ * so the pin is surfaced whenever a valid winner exists. */
172
+ asTopicMetadata(sessionKey) {
173
+ const topic = Number(sessionKey);
174
+ if (!Number.isFinite(topic))
175
+ return undefined;
176
+ const pin = this.pins().get(topic);
177
+ return pin ? { preferredMachine: pin.preferredMachine, pinned: true } : undefined;
178
+ }
179
+ /**
180
+ * §2D placement seeding with the FULL local-vs-replicated resolution (the
181
+ * N3 rule at the placement door): the LOCAL pin and the fold winner compare
182
+ * by HLC — a stale local pin never masks a fresher replicated move-intent,
183
+ * and a fresher replicated TOMBSTONE suppresses a stale local pin (the
184
+ * defect-2 fix applied to NEW placements, not just the reconciler). A local
185
+ * pin without an HLC uses the documented `updatedAt` fallback derivation.
186
+ */
187
+ effectiveTopicMetadata(sessionKey, local) {
188
+ const localMeta = local
189
+ ? { preferredMachine: local.preferredMachine, pinned: local.pinned }
190
+ : undefined;
191
+ const topic = Number(sessionKey);
192
+ const winner = Number.isFinite(topic) ? this.winners.get(String(topic)) : undefined;
193
+ if (!winner)
194
+ return localMeta;
195
+ const localHlc = local
196
+ ? (local.hlc ?? { physical: Date.parse(local.updatedAt) || 0, logical: 0, node: '' })
197
+ : null;
198
+ if (localHlc && compareHlc(localHlc, winner.hlc) >= 0)
199
+ return localMeta; // local is newer-or-equal
200
+ // The fold winner is newer: a tombstone / pinned:false resolves to NO pin
201
+ // (a stale local pin must not resurrect a cleared one); a valid put wins.
202
+ if (winner.data.op === 'delete' || winner.data.pinned !== true)
203
+ return undefined;
204
+ if (!isValidPinMachineId(winner.data.preferredMachine))
205
+ return localMeta;
206
+ return { preferredMachine: winner.data.preferredMachine, pinned: true };
207
+ }
208
+ /**
209
+ * Force the NEXT refresh to be a full-stream re-fold (offsets + winners
210
+ * cleared). Used by the explicit per-record re-admission surface (R-r4-1):
211
+ * a re-admitted record's bytes were already consumed by the incremental
212
+ * tail, so only a full re-fold can bring it back into the comparison.
213
+ */
214
+ resetFold() {
215
+ this.offsets = {};
216
+ this.winners.clear();
217
+ }
218
+ /** Read-only observability (route/status surfaces). */
219
+ status() {
220
+ let quarantined = 0;
221
+ try {
222
+ quarantined = this.d.quarantine.all().length;
223
+ }
224
+ catch { /* best-effort */ }
225
+ return {
226
+ lastFoldAt: this.lastFoldAtMs ? new Date(this.lastFoldAtMs).toISOString() : null,
227
+ totalFolds: this.totalFolds,
228
+ lastScannedBytes: this.lastScannedBytes,
229
+ recordKeys: this.winners.size,
230
+ truncationEpisodeOpen: this.truncationEpisodeOpen,
231
+ quarantined,
232
+ };
233
+ }
234
+ }
235
+ //# sourceMappingURL=TopicPinFoldView.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TopicPinFoldView.js","sourceRoot":"","sources":["../../src/core/TopicPinFoldView.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,46 @@
1
+ /**
2
+ * TopicPinMutation — the ONE-HLC pin mutation funnel (U4.1 §2B,
3
+ * docs/specs/u4-1-pin-persistence.md).
4
+ *
5
+ * THE CONTRACT (R-r2): every pin mutation mints ONE HLC and stamps BOTH the
6
+ * local `TopicPlacementPinStore.set()/clear()` AND the replicated journal
7
+ * emit (`buildTopicPinPut` / `buildTopicPinTombstone`) with that same stamp.
8
+ * The transfer route previously called `set()` WITHOUT the `hlc` argument
9
+ * (the routes.ts gap) — the local-vs-replicated comparison then degraded to a
10
+ * derived wall-clock and HLC-highest-wins was a symbol, not the state.
11
+ *
12
+ * UNPIN (fixes defect 2 — the live un-pin propagation bug): `clearPin` wires
13
+ * the previously ZERO-caller `buildTopicPinTombstone` at the clear()
14
+ * chokepoint, so a cleared pin can never be resurrected by a stale replicated
15
+ * PUT (`effectivePins()` / the fold honor tombstones by HLC order). A later
16
+ * re-pin is the same key with a newer HLC — the store's documented model.
17
+ *
18
+ * The emitter mints the HLC (its `clock.tick()` is the author-side authority);
19
+ * the funnel CAPTURES the minted stamp from inside the build closure and hands
20
+ * it to the local store. When emission is dark (`ws13PinReplicate` off /
21
+ * single-machine) the build closure never runs → no stamp is minted → the
22
+ * local write proceeds without one (today's exact behavior; the reconciler's
23
+ * documented `updatedAt` fallback derivation covers it).
24
+ */
25
+ import type { HlcTimestamp } from './HybridLogicalClock.js';
26
+ import type { TopicPlacementPinStore, TopicPinnedBy } from './TopicPlacementPinStore.js';
27
+ /** The emitter seam (ReplicatedRecordEmitter.emit — synchronous, never throws). */
28
+ export interface PinMutationEmitter {
29
+ emit(store: string, recordKey: string | null | undefined, build: (hlc: HlcTimestamp, origin: string, observed: HlcTimestamp | undefined) => Record<string, unknown> | null): void;
30
+ }
31
+ export interface PinMutationDeps {
32
+ pinStore: TopicPlacementPinStore;
33
+ /** Absent/null ⇒ replication un-wired (single-machine / early boot) — local-only write. */
34
+ emitter?: PinMutationEmitter | null;
35
+ now?: () => Date;
36
+ }
37
+ /** Pin a topic to a machine: ONE HLC stamps both the journal PUT and the local set(). */
38
+ export declare function setPinWithOneHlc(deps: PinMutationDeps, sessionKey: string, preferredMachine: string, pinned?: boolean, pinnedBy?: TopicPinnedBy): {
39
+ hlc?: HlcTimestamp;
40
+ };
41
+ /** Unpin a topic: the tombstone rides the SAME stamp the local clear is ordered by. */
42
+ export declare function clearPinWithTombstone(deps: PinMutationDeps, sessionKey: string): {
43
+ hadPin: boolean;
44
+ hlc?: HlcTimestamp;
45
+ };
46
+ //# sourceMappingURL=TopicPinMutation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TopicPinMutation.d.ts","sourceRoot":"","sources":["../../src/core/TopicPinMutation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,KAAK,EAAE,sBAAsB,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAQzF,mFAAmF;AACnF,MAAM,WAAW,kBAAkB;IACjC,IAAI,CACF,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACpC,KAAK,EAAE,CAAC,GAAG,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,YAAY,GAAG,SAAS,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GAC/G,IAAI,CAAC;CACT;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,sBAAsB,CAAC;IACjC,2FAA2F;IAC3F,OAAO,CAAC,EAAE,kBAAkB,GAAG,IAAI,CAAC;IACpC,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;CAClB;AAED,yFAAyF;AACzF,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,eAAe,EACrB,UAAU,EAAE,MAAM,EAClB,gBAAgB,EAAE,MAAM,EACxB,MAAM,UAAO,EACb,QAAQ,CAAC,EAAE,aAAa,GACvB;IAAE,GAAG,CAAC,EAAE,YAAY,CAAA;CAAE,CAgBxB;AAED,uFAAuF;AACvF,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,eAAe,EACrB,UAAU,EAAE,MAAM,GACjB;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,GAAG,CAAC,EAAE,YAAY,CAAA;CAAE,CAkBzC"}
@@ -0,0 +1,66 @@
1
+ /**
2
+ * TopicPinMutation — the ONE-HLC pin mutation funnel (U4.1 §2B,
3
+ * docs/specs/u4-1-pin-persistence.md).
4
+ *
5
+ * THE CONTRACT (R-r2): every pin mutation mints ONE HLC and stamps BOTH the
6
+ * local `TopicPlacementPinStore.set()/clear()` AND the replicated journal
7
+ * emit (`buildTopicPinPut` / `buildTopicPinTombstone`) with that same stamp.
8
+ * The transfer route previously called `set()` WITHOUT the `hlc` argument
9
+ * (the routes.ts gap) — the local-vs-replicated comparison then degraded to a
10
+ * derived wall-clock and HLC-highest-wins was a symbol, not the state.
11
+ *
12
+ * UNPIN (fixes defect 2 — the live un-pin propagation bug): `clearPin` wires
13
+ * the previously ZERO-caller `buildTopicPinTombstone` at the clear()
14
+ * chokepoint, so a cleared pin can never be resurrected by a stale replicated
15
+ * PUT (`effectivePins()` / the fold honor tombstones by HLC order). A later
16
+ * re-pin is the same key with a newer HLC — the store's documented model.
17
+ *
18
+ * The emitter mints the HLC (its `clock.tick()` is the author-side authority);
19
+ * the funnel CAPTURES the minted stamp from inside the build closure and hands
20
+ * it to the local store. When emission is dark (`ws13PinReplicate` off /
21
+ * single-machine) the build closure never runs → no stamp is minted → the
22
+ * local write proceeds without one (today's exact behavior; the reconciler's
23
+ * documented `updatedAt` fallback derivation covers it).
24
+ */
25
+ import { TOPIC_PIN_STORE_KEY, deriveTopicPinRecordKey, buildTopicPinPut, buildTopicPinTombstone, } from './TopicPinReplicatedStore.js';
26
+ /** Pin a topic to a machine: ONE HLC stamps both the journal PUT and the local set(). */
27
+ export function setPinWithOneHlc(deps, sessionKey, preferredMachine, pinned = true, pinnedBy) {
28
+ const topicNum = Number(sessionKey);
29
+ let minted;
30
+ if (deps.emitter && Number.isFinite(topicNum)) {
31
+ const recordKey = deriveTopicPinRecordKey(topicNum);
32
+ if (recordKey) {
33
+ const build = buildTopicPinPut(topicNum, preferredMachine, pinned);
34
+ deps.emitter.emit(TOPIC_PIN_STORE_KEY, recordKey, (hlc, origin, observed) => {
35
+ const data = build(hlc, origin, observed);
36
+ if (data !== null)
37
+ minted = hlc; // capture ONLY a stamp that actually rode a record
38
+ return data;
39
+ });
40
+ }
41
+ }
42
+ deps.pinStore.set(sessionKey, preferredMachine, pinned, minted, pinnedBy);
43
+ return minted !== undefined ? { hlc: minted } : {};
44
+ }
45
+ /** Unpin a topic: the tombstone rides the SAME stamp the local clear is ordered by. */
46
+ export function clearPinWithTombstone(deps, sessionKey) {
47
+ const hadPin = deps.pinStore.get(sessionKey) !== null;
48
+ const topicNum = Number(sessionKey);
49
+ let minted;
50
+ if (deps.emitter && Number.isFinite(topicNum)) {
51
+ const recordKey = deriveTopicPinRecordKey(topicNum);
52
+ if (recordKey) {
53
+ const deletedAt = (deps.now ?? (() => new Date()))().toISOString();
54
+ const build = buildTopicPinTombstone(topicNum, deletedAt);
55
+ deps.emitter.emit(TOPIC_PIN_STORE_KEY, recordKey, (hlc, origin, observed) => {
56
+ const data = build(hlc, origin, observed);
57
+ if (data !== null)
58
+ minted = hlc;
59
+ return data;
60
+ });
61
+ }
62
+ }
63
+ deps.pinStore.clear(sessionKey);
64
+ return minted !== undefined ? { hadPin, hlc: minted } : { hadPin };
65
+ }
66
+ //# sourceMappingURL=TopicPinMutation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TopicPinMutation.js","sourceRoot":"","sources":["../../src/core/TopicPinMutation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAIH,OAAO,EACL,mBAAmB,EACnB,uBAAuB,EACvB,gBAAgB,EAChB,sBAAsB,GACvB,MAAM,8BAA8B,CAAC;AAkBtC,yFAAyF;AACzF,MAAM,UAAU,gBAAgB,CAC9B,IAAqB,EACrB,UAAkB,EAClB,gBAAwB,EACxB,MAAM,GAAG,IAAI,EACb,QAAwB;IAExB,MAAM,QAAQ,GAAG,MAAM,CAAC,UAAU,CAAC,CAAC;IACpC,IAAI,MAAgC,CAAC;IACrC,IAAI,IAAI,CAAC,OAAO,IAAI,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC9C,MAAM,SAAS,GAAG,uBAAuB,CAAC,QAAQ,CAAC,CAAC;QACpD,IAAI,SAAS,EAAE,CAAC;YACd,MAAM,KAAK,GAAG,gBAAgB,CAAC,QAAQ,EAAE,gBAAgB,EAAE,MAAM,CAAC,CAAC;YACnE,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,mBAAmB,EAAE,SAAS,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE;gBAC1E,MAAM,IAAI,GAAG,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC;gBAC1C,IAAI,IAAI,KAAK,IAAI;oBAAE,MAAM,GAAG,GAAG,CAAC,CAAC,mDAAmD;gBACpF,OAAO,IAAI,CAAC;YACd,CAAC,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IACD,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,UAAU,EAAE,gBAAgB,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC;IAC1E,OAAO,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;AACrD,CAAC;AAED,uFAAuF;AACvF,MAAM,UAAU,qBAAqB,CACnC,IAAqB,EACrB,UAAkB;IAElB,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,UAAU,CAAC,KAAK,IAAI,CAAC;IACtD,MAAM,QAAQ,GAAG,MAAM,CAAC,UAAU,CAAC,CAAC;IACpC,IAAI,MAAgC,CAAC;IACrC,IAAI,IAAI,CAAC,OAAO,IAAI,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC9C,MAAM,SAAS,GAAG,uBAAuB,CAAC,QAAQ,CAAC,CAAC;QACpD,IAAI,SAAS,EAAE,CAAC;YACd,MAAM,SAAS,GAAG,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,WAAW,EAAE,CAAC;YACnE,MAAM,KAAK,GAAG,sBAAsB,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;YAC1D,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,mBAAmB,EAAE,SAAS,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE;gBAC1E,MAAM,IAAI,GAAG,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC;gBAC1C,IAAI,IAAI,KAAK,IAAI;oBAAE,MAAM,GAAG,GAAG,CAAC;gBAChC,OAAO,IAAI,CAAC;YACd,CAAC,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IACD,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;IAChC,OAAO,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC;AACrE,CAAC"}
@@ -26,6 +26,9 @@ import type { HlcTimestamp } from './HybridLogicalClock.js';
26
26
  export declare const TOPIC_PIN_RECORD_KIND = "topic-pin-record";
27
27
  /** The store key used by the emitter dark-gate + the rollback-unmerge getByStore wiring. */
28
28
  export declare const TOPIC_PIN_STORE_KEY = "topicPins";
29
+ /** The same clamp for consumers outside this module (the U4.1 fold view's second pass —
30
+ * ONE validation authority, never a re-implemented regex). */
31
+ export declare function isValidPinMachineId(id: unknown): id is string;
29
32
  /** recordKey = the cross-machine IDENTITY SURFACE = the topic id as a string (a pin is
30
33
  * per-topic; a re-pin to a different machine is the SAME key with a newer HLC). */
31
34
  export declare function deriveTopicPinRecordKey(topic: number | string): string | null;