instar 1.3.269 → 1.3.270

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 (81) hide show
  1. package/dist/commands/server.d.ts.map +1 -1
  2. package/dist/commands/server.js +9 -1
  3. package/dist/commands/server.js.map +1 -1
  4. package/dist/coordination/MandateAudit.d.ts +48 -0
  5. package/dist/coordination/MandateAudit.d.ts.map +1 -0
  6. package/dist/coordination/MandateAudit.js +95 -0
  7. package/dist/coordination/MandateAudit.js.map +1 -0
  8. package/dist/coordination/MandateGate.d.ts +48 -0
  9. package/dist/coordination/MandateGate.d.ts.map +1 -0
  10. package/dist/coordination/MandateGate.js +104 -0
  11. package/dist/coordination/MandateGate.js.map +1 -0
  12. package/dist/coordination/MandateStore.d.ts +60 -0
  13. package/dist/coordination/MandateStore.d.ts.map +1 -0
  14. package/dist/coordination/MandateStore.js +106 -0
  15. package/dist/coordination/MandateStore.js.map +1 -0
  16. package/dist/coordination/conditions.d.ts +32 -0
  17. package/dist/coordination/conditions.d.ts.map +1 -0
  18. package/dist/coordination/conditions.js +54 -0
  19. package/dist/coordination/conditions.js.map +1 -0
  20. package/dist/coordination/types.d.ts +74 -0
  21. package/dist/coordination/types.d.ts.map +1 -0
  22. package/dist/coordination/types.js +17 -0
  23. package/dist/coordination/types.js.map +1 -0
  24. package/dist/core/ApprovalLedger.d.ts +132 -0
  25. package/dist/core/ApprovalLedger.d.ts.map +1 -0
  26. package/dist/core/ApprovalLedger.js +191 -0
  27. package/dist/core/ApprovalLedger.js.map +1 -0
  28. package/dist/core/SessionBuildContextStore.d.ts +33 -0
  29. package/dist/core/SessionBuildContextStore.d.ts.map +1 -0
  30. package/dist/core/SessionBuildContextStore.js +107 -0
  31. package/dist/core/SessionBuildContextStore.js.map +1 -0
  32. package/dist/core/SessionManager.d.ts +4 -0
  33. package/dist/core/SessionManager.d.ts.map +1 -1
  34. package/dist/core/SessionManager.js +44 -3
  35. package/dist/core/SessionManager.js.map +1 -1
  36. package/dist/core/types.d.ts +11 -0
  37. package/dist/core/types.d.ts.map +1 -1
  38. package/dist/core/types.js.map +1 -1
  39. package/dist/feedback-factory/migration/immutableGuard.d.ts +87 -0
  40. package/dist/feedback-factory/migration/immutableGuard.d.ts.map +1 -0
  41. package/dist/feedback-factory/migration/immutableGuard.js +163 -0
  42. package/dist/feedback-factory/migration/immutableGuard.js.map +1 -0
  43. package/dist/feedback-factory/migration/importIntegrity.d.ts +112 -0
  44. package/dist/feedback-factory/migration/importIntegrity.d.ts.map +1 -0
  45. package/dist/feedback-factory/migration/importIntegrity.js +202 -0
  46. package/dist/feedback-factory/migration/importIntegrity.js.map +1 -0
  47. package/dist/feedback-factory/monitor/parityMonitor.d.ts +75 -0
  48. package/dist/feedback-factory/monitor/parityMonitor.d.ts.map +1 -0
  49. package/dist/feedback-factory/monitor/parityMonitor.js +98 -0
  50. package/dist/feedback-factory/monitor/parityMonitor.js.map +1 -0
  51. package/dist/feedback-factory/monitor/parityMonitorStore.d.ts +44 -0
  52. package/dist/feedback-factory/monitor/parityMonitorStore.d.ts.map +1 -0
  53. package/dist/feedback-factory/monitor/parityMonitorStore.js +80 -0
  54. package/dist/feedback-factory/monitor/parityMonitorStore.js.map +1 -0
  55. package/dist/scaffold/templates.d.ts.map +1 -1
  56. package/dist/scaffold/templates.js +11 -0
  57. package/dist/scaffold/templates.js.map +1 -1
  58. package/dist/server/AgentServer.d.ts +6 -0
  59. package/dist/server/AgentServer.d.ts.map +1 -1
  60. package/dist/server/AgentServer.js +94 -1
  61. package/dist/server/AgentServer.js.map +1 -1
  62. package/dist/server/CapabilityIndex.d.ts.map +1 -1
  63. package/dist/server/CapabilityIndex.js +29 -0
  64. package/dist/server/CapabilityIndex.js.map +1 -1
  65. package/dist/server/routes.d.ts +11 -0
  66. package/dist/server/routes.d.ts.map +1 -1
  67. package/dist/server/routes.js +255 -0
  68. package/dist/server/routes.js.map +1 -1
  69. package/package.json +1 -1
  70. package/src/data/builtin-manifest.json +48 -48
  71. package/src/scaffold/templates.ts +11 -0
  72. package/upgrades/1.3.270.md +106 -0
  73. package/upgrades/approval-ledger-phase2.eli16.md +56 -0
  74. package/upgrades/feedback-phase3-monitor.eli16.md +51 -0
  75. package/upgrades/feedback-phase4-integrity.eli16.md +54 -0
  76. package/upgrades/respawn-build-context-reestablishment.eli16.md +5 -0
  77. package/upgrades/side-effects/approval-ledger-phase2.md +82 -0
  78. package/upgrades/side-effects/coordination-mandate-enforcement.md +86 -0
  79. package/upgrades/side-effects/feedback-phase3-monitor.md +37 -0
  80. package/upgrades/side-effects/feedback-phase4-integrity.md +35 -0
  81. package/upgrades/side-effects/respawn-build-context-reestablishment.md +81 -0
@@ -0,0 +1,112 @@
1
+ /**
2
+ * importIntegrity.ts — the integrity-safe import core (spec §2.4).
3
+ *
4
+ * "Row counts + spot-check" is too weak to catch silent corruption of the irreplaceable
5
+ * curated state. This module is the pure, deterministic core of the AS-IS import gate;
6
+ * the real Prisma adapter wraps these in one transaction (parent-before-child FK order)
7
+ * and runs the synthetic post-import insert. Keeping the logic pure makes the Phase-2
8
+ * gate fully unit-testable without a database.
9
+ *
10
+ * The gate (spec §2.4): per-row curated-field checksums match (in vs out) + pre-import
11
+ * fingerprint-uniqueness resolved + schema-equivalence holds + FK referential integrity
12
+ * + a sequence reset computed so the next post-cutover insert cannot collide (P2002).
13
+ */
14
+ import type { Cluster, FeedbackItem } from '../processor/types.js';
15
+ /**
16
+ * Curated cluster fields whose exact preservation the checksum guards. The cluster table
17
+ * is curated human/LLM judgment, not a cache — every one of these must survive AS-IS.
18
+ */
19
+ export declare const CURATED_CLUSTER_FIELDS: readonly ["clusterId", "title", "description", "type", "status", "fingerprint", "recurrenceCount", "reportCount", "createdAt", "fixedInVersion", "fixAppliedAt", "dispatchedAt", "governanceNotes", "processingNotes", "actionTaken", "researchNotes", "chronicCount"];
20
+ /** Curated feedback fields (the report rows linked to clusters). */
21
+ export declare const CURATED_FEEDBACK_FIELDS: readonly ["feedbackId", "title", "description", "type", "status", "receivedAt", "instarVersion"];
22
+ /** Deterministic checksum over the named fields of a row (field order fixed by `fields`). */
23
+ export declare function curatedChecksum(row: Record<string, unknown>, fields: readonly string[]): string;
24
+ export declare function clusterChecksum(cluster: Cluster): string;
25
+ export declare function feedbackChecksum(item: FeedbackItem): string;
26
+ export interface ChecksumMismatch {
27
+ id: string;
28
+ kind: 'cluster' | 'feedback';
29
+ reason: 'missing-in-target' | 'extra-in-target' | 'checksum-differs';
30
+ sourceChecksum?: string;
31
+ targetChecksum?: string;
32
+ }
33
+ /**
34
+ * Compare source rows (Dawn's export) against imported rows (canonical instance) by id,
35
+ * field-by-field via checksum. Returns every divergence — empty array == clean import.
36
+ */
37
+ export declare function verifyImportChecksums(source: {
38
+ clusters: Cluster[];
39
+ feedback: FeedbackItem[];
40
+ }, target: {
41
+ clusters: Cluster[];
42
+ feedback: FeedbackItem[];
43
+ }): ChecksumMismatch[];
44
+ /** A set of clusters that collide on `fingerprint` (the @unique constraint would abort the import). */
45
+ export interface FingerprintCollision {
46
+ fingerprint: string;
47
+ clusterIds: string[];
48
+ }
49
+ /**
50
+ * Pre-import uniqueness scan on the SOURCE: curated history may hold two clusters sharing a
51
+ * fingerprint (after manual merges/edits). An AS-IS import would abort on @unique mid-txn or
52
+ * silently collapse them — detect + surface BEFORE importing so a human resolves it.
53
+ */
54
+ export declare function scanFingerprintUniqueness(clusters: Cluster[]): FingerprintCollision[];
55
+ /** A field whose value-domain differs between Dawn's schema and the canonical instance. */
56
+ export interface SchemaDivergence {
57
+ field: string;
58
+ kind: 'unknown-status-value' | 'type-mismatch' | 'missing-field';
59
+ detail: string;
60
+ }
61
+ export interface SchemaDescriptor {
62
+ /** Allowed lifecycle status values (enum or string union). */
63
+ statusValues: string[];
64
+ /** Field → declared type, for the fields the processor reads. */
65
+ fieldTypes: Record<string, string>;
66
+ }
67
+ /**
68
+ * Schema-equivalence assertion: every status value the SOURCE uses must be accepted by the
69
+ * TARGET, and the field types must line up. Mismatched enums or a status the target rejects
70
+ * would corrupt the import silently — surface it before the transaction.
71
+ */
72
+ export declare function assertSchemaEquivalence(source: SchemaDescriptor, target: SchemaDescriptor): SchemaDivergence[];
73
+ /** A feedback row pointing at a cluster that does not exist in the import set. */
74
+ export interface DanglingRef {
75
+ feedbackId: string;
76
+ clusterId: string;
77
+ }
78
+ /**
79
+ * FK referential-integrity check (not just row counts): every feedback row that claims a
80
+ * clusterId must resolve to an imported cluster. Run AFTER the parent-before-child import
81
+ * to prove no dangling references survived.
82
+ */
83
+ export declare function checkReferentialIntegrity(clusters: Cluster[], feedback: FeedbackItem[]): DanglingRef[];
84
+ /**
85
+ * The next auto-increment value to set after importing explicit PKs, so the next NEW
86
+ * post-cutover insert cannot collide (P2002). Returns maxNumericId + 1, or 1 if no numeric
87
+ * ids were imported. Non-numeric ids are ignored (cuid/uuid PKs need no sequence reset).
88
+ */
89
+ export declare function planSequenceReset(importedIds: Array<string | number>): number;
90
+ export interface IntegrityReport {
91
+ fingerprintCollisions: FingerprintCollision[];
92
+ schemaDivergences: SchemaDivergence[];
93
+ checksumMismatches: ChecksumMismatch[];
94
+ danglingRefs: DanglingRef[];
95
+ sequenceResetTo: number;
96
+ /** True only when EVERY check is clean — the Phase-2/4 import gate. */
97
+ passed: boolean;
98
+ }
99
+ /**
100
+ * Run the full integrity gate over a source export and its imported copy. `passed` is the
101
+ * single Phase-2/4 gate boolean; any non-empty divergence list fails it.
102
+ */
103
+ export declare function runIntegrityGate(source: {
104
+ clusters: Cluster[];
105
+ feedback: FeedbackItem[];
106
+ schema: SchemaDescriptor;
107
+ }, target: {
108
+ clusters: Cluster[];
109
+ feedback: FeedbackItem[];
110
+ schema: SchemaDescriptor;
111
+ }): IntegrityReport;
112
+ //# sourceMappingURL=importIntegrity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"importIntegrity.d.ts","sourceRoot":"","sources":["../../../src/feedback-factory/migration/importIntegrity.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,OAAO,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAEnE;;;GAGG;AACH,eAAO,MAAM,sBAAsB,wQAkBzB,CAAC;AAEX,oEAAoE;AACpE,eAAO,MAAM,uBAAuB,kGAQ1B,CAAC;AAcX,6FAA6F;AAC7F,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAS/F;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CAExD;AACD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,YAAY,GAAG,MAAM,CAE3D;AAED,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,SAAS,GAAG,UAAU,CAAC;IAC7B,MAAM,EAAE,mBAAmB,GAAG,iBAAiB,GAAG,kBAAkB,CAAC;IACrE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE;IAAE,QAAQ,EAAE,OAAO,EAAE,CAAC;IAAC,QAAQ,EAAE,YAAY,EAAE,CAAA;CAAE,EACzD,MAAM,EAAE;IAAE,QAAQ,EAAE,OAAO,EAAE,CAAC;IAAC,QAAQ,EAAE,YAAY,EAAE,CAAA;CAAE,GACxD,gBAAgB,EAAE,CAiCpB;AAED,uGAAuG;AACvG,MAAM,WAAW,oBAAoB;IACnC,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,EAAE,CAAC;CACtB;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,oBAAoB,EAAE,CAcrF;AAED,2FAA2F;AAC3F,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,sBAAsB,GAAG,eAAe,GAAG,eAAe,CAAC;IACjE,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,gBAAgB;IAC/B,8DAA8D;IAC9D,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,iEAAiE;IACjE,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACpC;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,gBAAgB,EAAE,MAAM,EAAE,gBAAgB,GAAG,gBAAgB,EAAE,CAiB9G;AAED,kFAAkF;AAClF,MAAM,WAAW,WAAW;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,QAAQ,EAAE,YAAY,EAAE,GAAG,WAAW,EAAE,CAUtG;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,GAAG,MAAM,CAO7E;AAED,MAAM,WAAW,eAAe;IAC9B,qBAAqB,EAAE,oBAAoB,EAAE,CAAC;IAC9C,iBAAiB,EAAE,gBAAgB,EAAE,CAAC;IACtC,kBAAkB,EAAE,gBAAgB,EAAE,CAAC;IACvC,YAAY,EAAE,WAAW,EAAE,CAAC;IAC5B,eAAe,EAAE,MAAM,CAAC;IACxB,uEAAuE;IACvE,MAAM,EAAE,OAAO,CAAC;CACjB;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE;IAAE,QAAQ,EAAE,OAAO,EAAE,CAAC;IAAC,QAAQ,EAAE,YAAY,EAAE,CAAC;IAAC,MAAM,EAAE,gBAAgB,CAAA;CAAE,EACnF,MAAM,EAAE;IAAE,QAAQ,EAAE,OAAO,EAAE,CAAC;IAAC,QAAQ,EAAE,YAAY,EAAE,CAAC;IAAC,MAAM,EAAE,gBAAgB,CAAA;CAAE,GAClF,eAAe,CAYjB"}
@@ -0,0 +1,202 @@
1
+ /**
2
+ * importIntegrity.ts — the integrity-safe import core (spec §2.4).
3
+ *
4
+ * "Row counts + spot-check" is too weak to catch silent corruption of the irreplaceable
5
+ * curated state. This module is the pure, deterministic core of the AS-IS import gate;
6
+ * the real Prisma adapter wraps these in one transaction (parent-before-child FK order)
7
+ * and runs the synthetic post-import insert. Keeping the logic pure makes the Phase-2
8
+ * gate fully unit-testable without a database.
9
+ *
10
+ * The gate (spec §2.4): per-row curated-field checksums match (in vs out) + pre-import
11
+ * fingerprint-uniqueness resolved + schema-equivalence holds + FK referential integrity
12
+ * + a sequence reset computed so the next post-cutover insert cannot collide (P2002).
13
+ */
14
+ import { createHash } from 'node:crypto';
15
+ /**
16
+ * Curated cluster fields whose exact preservation the checksum guards. The cluster table
17
+ * is curated human/LLM judgment, not a cache — every one of these must survive AS-IS.
18
+ */
19
+ export const CURATED_CLUSTER_FIELDS = [
20
+ 'clusterId',
21
+ 'title',
22
+ 'description',
23
+ 'type',
24
+ 'status',
25
+ 'fingerprint',
26
+ 'recurrenceCount',
27
+ 'reportCount',
28
+ 'createdAt',
29
+ 'fixedInVersion',
30
+ 'fixAppliedAt',
31
+ 'dispatchedAt',
32
+ 'governanceNotes',
33
+ 'processingNotes',
34
+ 'actionTaken',
35
+ 'researchNotes',
36
+ 'chronicCount',
37
+ ];
38
+ /** Curated feedback fields (the report rows linked to clusters). */
39
+ export const CURATED_FEEDBACK_FIELDS = [
40
+ 'feedbackId',
41
+ 'title',
42
+ 'description',
43
+ 'type',
44
+ 'status',
45
+ 'receivedAt',
46
+ 'instarVersion',
47
+ ];
48
+ /**
49
+ * Canonicalize a value for checksumming so that semantically-identical rows hash equal
50
+ * across the source→target hop. null and undefined and "" all collapse to "" (the
51
+ * null-vs-empty governance-note distinction is asserted separately by schema-equivalence;
52
+ * the checksum must not flap on it). Everything else is JSON-stringified.
53
+ */
54
+ function canonicalValue(v) {
55
+ if (v === null || v === undefined || v === '')
56
+ return '';
57
+ if (typeof v === 'number' && Number.isNaN(v))
58
+ return 'NaN';
59
+ return JSON.stringify(v);
60
+ }
61
+ /** Deterministic checksum over the named fields of a row (field order fixed by `fields`). */
62
+ export function curatedChecksum(row, fields) {
63
+ const h = createHash('sha256');
64
+ for (const f of fields) {
65
+ h.update(f);
66
+ h.update('\x1f'); // unit separator: field-name | value
67
+ h.update(canonicalValue(row[f]));
68
+ h.update('\x1e'); // record separator after each field (no cross-field run ambiguity)
69
+ }
70
+ return h.digest('hex');
71
+ }
72
+ export function clusterChecksum(cluster) {
73
+ return curatedChecksum(cluster, CURATED_CLUSTER_FIELDS);
74
+ }
75
+ export function feedbackChecksum(item) {
76
+ return curatedChecksum(item, CURATED_FEEDBACK_FIELDS);
77
+ }
78
+ /**
79
+ * Compare source rows (Dawn's export) against imported rows (canonical instance) by id,
80
+ * field-by-field via checksum. Returns every divergence — empty array == clean import.
81
+ */
82
+ export function verifyImportChecksums(source, target) {
83
+ const out = [];
84
+ const verify = (kind, idField, src, tgt, checksum) => {
85
+ const tgtById = new Map(tgt.map((r) => [String(r[idField]), r]));
86
+ const seen = new Set();
87
+ for (const s of src) {
88
+ const id = String(s[idField]);
89
+ seen.add(id);
90
+ const t = tgtById.get(id);
91
+ if (!t) {
92
+ out.push({ id, kind, reason: 'missing-in-target', sourceChecksum: checksum(s) });
93
+ continue;
94
+ }
95
+ const sc = checksum(s);
96
+ const tc = checksum(t);
97
+ if (sc !== tc)
98
+ out.push({ id, kind, reason: 'checksum-differs', sourceChecksum: sc, targetChecksum: tc });
99
+ }
100
+ for (const t of tgt) {
101
+ const id = String(t[idField]);
102
+ if (!seen.has(id))
103
+ out.push({ id, kind, reason: 'extra-in-target', targetChecksum: checksum(t) });
104
+ }
105
+ };
106
+ verify('cluster', 'clusterId', source.clusters, target.clusters, (c) => clusterChecksum(c));
107
+ verify('feedback', 'feedbackId', source.feedback, target.feedback, (f) => feedbackChecksum(f));
108
+ return out;
109
+ }
110
+ /**
111
+ * Pre-import uniqueness scan on the SOURCE: curated history may hold two clusters sharing a
112
+ * fingerprint (after manual merges/edits). An AS-IS import would abort on @unique mid-txn or
113
+ * silently collapse them — detect + surface BEFORE importing so a human resolves it.
114
+ */
115
+ export function scanFingerprintUniqueness(clusters) {
116
+ const byFp = new Map();
117
+ for (const c of clusters) {
118
+ const fp = typeof c.fingerprint === 'string' ? c.fingerprint : '';
119
+ if (!fp)
120
+ continue; // a missing fingerprint isn't a @unique collision
121
+ const arr = byFp.get(fp) ?? [];
122
+ arr.push(c.clusterId);
123
+ byFp.set(fp, arr);
124
+ }
125
+ const out = [];
126
+ for (const [fingerprint, clusterIds] of byFp) {
127
+ if (clusterIds.length > 1)
128
+ out.push({ fingerprint, clusterIds });
129
+ }
130
+ return out;
131
+ }
132
+ /**
133
+ * Schema-equivalence assertion: every status value the SOURCE uses must be accepted by the
134
+ * TARGET, and the field types must line up. Mismatched enums or a status the target rejects
135
+ * would corrupt the import silently — surface it before the transaction.
136
+ */
137
+ export function assertSchemaEquivalence(source, target) {
138
+ const out = [];
139
+ const targetStatus = new Set(target.statusValues);
140
+ for (const s of source.statusValues) {
141
+ if (!targetStatus.has(s)) {
142
+ out.push({ field: 'status', kind: 'unknown-status-value', detail: `target does not accept status "${s}"` });
143
+ }
144
+ }
145
+ for (const [field, srcType] of Object.entries(source.fieldTypes)) {
146
+ const tgtType = target.fieldTypes[field];
147
+ if (tgtType === undefined) {
148
+ out.push({ field, kind: 'missing-field', detail: `target schema has no field "${field}"` });
149
+ }
150
+ else if (tgtType !== srcType) {
151
+ out.push({ field, kind: 'type-mismatch', detail: `source ${srcType} vs target ${tgtType}` });
152
+ }
153
+ }
154
+ return out;
155
+ }
156
+ /**
157
+ * FK referential-integrity check (not just row counts): every feedback row that claims a
158
+ * clusterId must resolve to an imported cluster. Run AFTER the parent-before-child import
159
+ * to prove no dangling references survived.
160
+ */
161
+ export function checkReferentialIntegrity(clusters, feedback) {
162
+ const clusterIds = new Set(clusters.map((c) => c.clusterId));
163
+ const out = [];
164
+ for (const f of feedback) {
165
+ const cid = f.clusterId;
166
+ if (typeof cid === 'string' && cid.length > 0 && !clusterIds.has(cid)) {
167
+ out.push({ feedbackId: f.feedbackId, clusterId: cid });
168
+ }
169
+ }
170
+ return out;
171
+ }
172
+ /**
173
+ * The next auto-increment value to set after importing explicit PKs, so the next NEW
174
+ * post-cutover insert cannot collide (P2002). Returns maxNumericId + 1, or 1 if no numeric
175
+ * ids were imported. Non-numeric ids are ignored (cuid/uuid PKs need no sequence reset).
176
+ */
177
+ export function planSequenceReset(importedIds) {
178
+ let max = 0;
179
+ for (const id of importedIds) {
180
+ const n = typeof id === 'number' ? id : Number(id);
181
+ if (Number.isFinite(n) && n > max)
182
+ max = Math.floor(n);
183
+ }
184
+ return max + 1;
185
+ }
186
+ /**
187
+ * Run the full integrity gate over a source export and its imported copy. `passed` is the
188
+ * single Phase-2/4 gate boolean; any non-empty divergence list fails it.
189
+ */
190
+ export function runIntegrityGate(source, target) {
191
+ const fingerprintCollisions = scanFingerprintUniqueness(source.clusters);
192
+ const schemaDivergences = assertSchemaEquivalence(source.schema, target.schema);
193
+ const checksumMismatches = verifyImportChecksums(source, target);
194
+ const danglingRefs = checkReferentialIntegrity(target.clusters, target.feedback);
195
+ const sequenceResetTo = planSequenceReset(target.clusters.map((c) => c.clusterId));
196
+ const passed = fingerprintCollisions.length === 0 &&
197
+ schemaDivergences.length === 0 &&
198
+ checksumMismatches.length === 0 &&
199
+ danglingRefs.length === 0;
200
+ return { fingerprintCollisions, schemaDivergences, checksumMismatches, danglingRefs, sequenceResetTo, passed };
201
+ }
202
+ //# sourceMappingURL=importIntegrity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"importIntegrity.js","sourceRoot":"","sources":["../../../src/feedback-factory/migration/importIntegrity.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,75 @@
1
+ /**
2
+ * parityMonitor.ts — the Phase-3 live dual-forward parity MONITOR + cutover gate (spec §2.5).
3
+ *
4
+ * The per-pass comparison already exists (processor/parity.ts → ParityResult over the
5
+ * order-independent invariants: per-report fingerprint, terminal-status, recurrence/cycling
6
+ * counts — NOT raw cluster membership, which is order-dependent and would flap on benign
7
+ * arrival-order noise). What Phase 3 needs ON TOP of one comparison is the WINDOW: run the
8
+ * comparison repeatedly over real dual-forward traffic and only clear the cutover when a
9
+ * genuine zero-divergence window has elapsed.
10
+ *
11
+ * This monitor records each pass, tracks the current zero-divergence streak (ANY divergent
12
+ * pass resets it), and exposes the single structural gate `gate()` → cleared/blocked. That
13
+ * `cleared` boolean is the objective `parity-zero-divergence` condition the Coordination
14
+ * Mandate's `execute-cutover` authority reads — the agent cannot fake it; the window decides.
15
+ *
16
+ * Structure > Willpower: cutover is blocked by the gate, not by an agent deciding "looks
17
+ * good". The pure core here is fully unit-testable; a thin durable wrapper (below) persists
18
+ * passes so the window survives a restart, since it spans real traffic time.
19
+ */
20
+ import type { ParityResult } from '../processor/parity.js';
21
+ /** One recorded comparison pass over a dual-forward traffic slice. */
22
+ export interface MonitorPass {
23
+ /** ISO timestamp of this pass. */
24
+ at: string;
25
+ /** Clusters compared in this pass (the meaningful-sample input). */
26
+ clustersCompared: number;
27
+ /** Total divergences this pass (fingerprint + outcome). 0 == clean. */
28
+ divergences: number;
29
+ /** Convenience mirror of `divergences > 0`. */
30
+ divergent: boolean;
31
+ }
32
+ /** The window a clean streak must satisfy before the cutover gate clears. */
33
+ export interface CutoverGatePolicy {
34
+ /** Consecutive zero-divergence passes required (default 3). */
35
+ requiredCleanPasses: number;
36
+ /** The clean streak must span at least this much REAL time (default 1h). */
37
+ minWindowMs: number;
38
+ /** Total clusters compared across the clean streak — a meaningful sample (default 1). */
39
+ minClustersObserved: number;
40
+ }
41
+ export declare const DEFAULT_GATE_POLICY: CutoverGatePolicy;
42
+ export interface CutoverGateStatus {
43
+ /** The structural signal: true only when the zero-divergence window is genuinely satisfied. */
44
+ cleared: boolean;
45
+ reason: string;
46
+ cleanPasses: number;
47
+ windowMs: number;
48
+ clustersObserved: number;
49
+ /** ISO of the most recent divergent pass (the streak reset point), or null if never. */
50
+ lastDivergentAt: string | null;
51
+ }
52
+ /**
53
+ * Records parity passes and computes the cutover gate over the current zero-divergence
54
+ * streak. Pure + clock-injected (gate(now) takes the current time) → fully testable.
55
+ */
56
+ export declare class ParityMonitor {
57
+ private readonly policy;
58
+ private readonly _passes;
59
+ constructor(policy?: Partial<CutoverGatePolicy>);
60
+ /** Feed a raw pass record. */
61
+ record(pass: MonitorPass): void;
62
+ /** Convenience: record straight from a ParityResult + the pass timestamp. */
63
+ recordResult(result: ParityResult, at: string): void;
64
+ get passes(): readonly MonitorPass[];
65
+ /**
66
+ * The structural cutover gate. `cleared` is true only when the trailing run of passes is:
67
+ * - all clean (zero divergence), AND
68
+ * - at least `requiredCleanPasses` long, AND
69
+ * - spanning at least `minWindowMs` of real time (first-clean-pass → now), AND
70
+ * - covering at least `minClustersObserved` clusters in total.
71
+ * Any divergent pass resets the streak. No passes → blocked.
72
+ */
73
+ gate(now: string): CutoverGateStatus;
74
+ }
75
+ //# sourceMappingURL=parityMonitor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parityMonitor.d.ts","sourceRoot":"","sources":["../../../src/feedback-factory/monitor/parityMonitor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAE3D,sEAAsE;AACtE,MAAM,WAAW,WAAW;IAC1B,kCAAkC;IAClC,EAAE,EAAE,MAAM,CAAC;IACX,oEAAoE;IACpE,gBAAgB,EAAE,MAAM,CAAC;IACzB,uEAAuE;IACvE,WAAW,EAAE,MAAM,CAAC;IACpB,+CAA+C;IAC/C,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,6EAA6E;AAC7E,MAAM,WAAW,iBAAiB;IAChC,+DAA+D;IAC/D,mBAAmB,EAAE,MAAM,CAAC;IAC5B,4EAA4E;IAC5E,WAAW,EAAE,MAAM,CAAC;IACpB,yFAAyF;IACzF,mBAAmB,EAAE,MAAM,CAAC;CAC7B;AAED,eAAO,MAAM,mBAAmB,EAAE,iBAIjC,CAAC;AAEF,MAAM,WAAW,iBAAiB;IAChC,+FAA+F;IAC/F,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,gBAAgB,EAAE,MAAM,CAAC;IACzB,wFAAwF;IACxF,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC;AAMD;;;GAGG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAoB;IAC3C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqB;gBAEjC,MAAM,GAAE,OAAO,CAAC,iBAAiB,CAAM;IAInD,8BAA8B;IAC9B,MAAM,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI;IAI/B,6EAA6E;IAC7E,YAAY,CAAC,MAAM,EAAE,YAAY,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI;IAKpD,IAAI,MAAM,IAAI,SAAS,WAAW,EAAE,CAEnC;IAED;;;;;;;OAOG;IACH,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,iBAAiB;CAwCrC"}
@@ -0,0 +1,98 @@
1
+ /**
2
+ * parityMonitor.ts — the Phase-3 live dual-forward parity MONITOR + cutover gate (spec §2.5).
3
+ *
4
+ * The per-pass comparison already exists (processor/parity.ts → ParityResult over the
5
+ * order-independent invariants: per-report fingerprint, terminal-status, recurrence/cycling
6
+ * counts — NOT raw cluster membership, which is order-dependent and would flap on benign
7
+ * arrival-order noise). What Phase 3 needs ON TOP of one comparison is the WINDOW: run the
8
+ * comparison repeatedly over real dual-forward traffic and only clear the cutover when a
9
+ * genuine zero-divergence window has elapsed.
10
+ *
11
+ * This monitor records each pass, tracks the current zero-divergence streak (ANY divergent
12
+ * pass resets it), and exposes the single structural gate `gate()` → cleared/blocked. That
13
+ * `cleared` boolean is the objective `parity-zero-divergence` condition the Coordination
14
+ * Mandate's `execute-cutover` authority reads — the agent cannot fake it; the window decides.
15
+ *
16
+ * Structure > Willpower: cutover is blocked by the gate, not by an agent deciding "looks
17
+ * good". The pure core here is fully unit-testable; a thin durable wrapper (below) persists
18
+ * passes so the window survives a restart, since it spans real traffic time.
19
+ */
20
+ export const DEFAULT_GATE_POLICY = {
21
+ requiredCleanPasses: 3,
22
+ minWindowMs: 60 * 60 * 1000,
23
+ minClustersObserved: 1,
24
+ };
25
+ function passDivergences(r) {
26
+ return r.fingerprintDivergences.length + r.outcomeDivergences.length;
27
+ }
28
+ /**
29
+ * Records parity passes and computes the cutover gate over the current zero-divergence
30
+ * streak. Pure + clock-injected (gate(now) takes the current time) → fully testable.
31
+ */
32
+ export class ParityMonitor {
33
+ policy;
34
+ _passes = [];
35
+ constructor(policy = {}) {
36
+ this.policy = { ...DEFAULT_GATE_POLICY, ...policy };
37
+ }
38
+ /** Feed a raw pass record. */
39
+ record(pass) {
40
+ this._passes.push(pass);
41
+ }
42
+ /** Convenience: record straight from a ParityResult + the pass timestamp. */
43
+ recordResult(result, at) {
44
+ const divergences = passDivergences(result);
45
+ this._passes.push({ at, clustersCompared: result.clustersCompared, divergences, divergent: divergences > 0 });
46
+ }
47
+ get passes() {
48
+ return this._passes;
49
+ }
50
+ /**
51
+ * The structural cutover gate. `cleared` is true only when the trailing run of passes is:
52
+ * - all clean (zero divergence), AND
53
+ * - at least `requiredCleanPasses` long, AND
54
+ * - spanning at least `minWindowMs` of real time (first-clean-pass → now), AND
55
+ * - covering at least `minClustersObserved` clusters in total.
56
+ * Any divergent pass resets the streak. No passes → blocked.
57
+ */
58
+ gate(now) {
59
+ // Find the index just after the most recent divergent pass — the start of the clean streak.
60
+ let streakStart = 0;
61
+ let lastDivergentAt = null;
62
+ for (let i = this._passes.length - 1; i >= 0; i--) {
63
+ if (this._passes[i].divergent) {
64
+ streakStart = i + 1;
65
+ lastDivergentAt = this._passes[i].at;
66
+ break;
67
+ }
68
+ }
69
+ const streak = this._passes.slice(streakStart);
70
+ const cleanPasses = streak.length;
71
+ const clustersObserved = streak.reduce((s, p) => s + p.clustersCompared, 0);
72
+ const nowMs = Date.parse(now);
73
+ const firstCleanMs = cleanPasses > 0 ? Date.parse(streak[0].at) : nowMs;
74
+ const windowMs = Number.isNaN(nowMs) || Number.isNaN(firstCleanMs) ? 0 : Math.max(0, nowMs - firstCleanMs);
75
+ const reasons = [];
76
+ if (cleanPasses < this.policy.requiredCleanPasses) {
77
+ reasons.push(`need ${this.policy.requiredCleanPasses} consecutive clean passes, have ${cleanPasses}`);
78
+ }
79
+ if (windowMs < this.policy.minWindowMs) {
80
+ reasons.push(`clean window ${Math.round(windowMs / 1000)}s < required ${Math.round(this.policy.minWindowMs / 1000)}s`);
81
+ }
82
+ if (clustersObserved < this.policy.minClustersObserved) {
83
+ reasons.push(`observed ${clustersObserved} clusters < required ${this.policy.minClustersObserved}`);
84
+ }
85
+ const cleared = reasons.length === 0;
86
+ return {
87
+ cleared,
88
+ reason: cleared
89
+ ? `zero-divergence window satisfied: ${cleanPasses} clean passes over ${Math.round(windowMs / 1000)}s, ${clustersObserved} clusters`
90
+ : reasons.join('; '),
91
+ cleanPasses,
92
+ windowMs,
93
+ clustersObserved,
94
+ lastDivergentAt,
95
+ };
96
+ }
97
+ }
98
+ //# sourceMappingURL=parityMonitor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parityMonitor.js","sourceRoot":"","sources":["../../../src/feedback-factory/monitor/parityMonitor.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,44 @@
1
+ /**
2
+ * parityMonitorStore.ts — durable persistence for the Phase-3 parity monitor.
3
+ *
4
+ * The zero-divergence window that gates cutover spans HOURS of real dual-forward traffic.
5
+ * If the monitor's passes lived only in memory, a server restart mid-window would silently
6
+ * reset the streak — at best re-blocking cutover (conservative, survivable), at worst making
7
+ * the gate's `windowMs` meaningless across the very restart it must survive. So passes are
8
+ * appended to a JSONL and reloaded on construction: the window is durable across restarts.
9
+ *
10
+ * Append-only JSONL (one MonitorPass per line) matches Instar's file-based-state convention
11
+ * and is crash-safe (a torn final line is skipped on reload, never corrupting the rest).
12
+ * The pure ParityMonitor stays the gate brain; this is the I/O shell around it.
13
+ */
14
+ import { type CutoverGatePolicy, type CutoverGateStatus, type MonitorPass } from './parityMonitor.js';
15
+ import type { ParityResult } from '../processor/parity.js';
16
+ /** Injectable I/O so unit tests can drive the durable layer without touching disk. */
17
+ export interface PassPersistence {
18
+ /** Load all previously-recorded passes, oldest-first. */
19
+ load(): MonitorPass[];
20
+ /** Durably append one pass. */
21
+ append(pass: MonitorPass): void;
22
+ }
23
+ /** The default JSONL-on-disk persistence (append-only, torn-line tolerant). */
24
+ export declare class JsonlPassPersistence implements PassPersistence {
25
+ private readonly path;
26
+ constructor(path: string);
27
+ load(): MonitorPass[];
28
+ append(pass: MonitorPass): void;
29
+ }
30
+ /**
31
+ * A ParityMonitor whose passes survive a restart. Reloads persisted passes on construction,
32
+ * then every record() both persists and feeds the in-memory monitor. gate() delegates to the
33
+ * monitor over the full (reloaded + new) history.
34
+ */
35
+ export declare class DurableParityMonitor {
36
+ private readonly monitor;
37
+ private readonly persistence;
38
+ constructor(persistence: PassPersistence, policy?: Partial<CutoverGatePolicy>);
39
+ record(pass: MonitorPass): void;
40
+ recordResult(result: ParityResult, at: string): void;
41
+ gate(now: string): CutoverGateStatus;
42
+ get passes(): readonly MonitorPass[];
43
+ }
44
+ //# sourceMappingURL=parityMonitorStore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parityMonitorStore.d.ts","sourceRoot":"","sources":["../../../src/feedback-factory/monitor/parityMonitorStore.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAIH,OAAO,EAAiB,KAAK,iBAAiB,EAAE,KAAK,iBAAiB,EAAE,KAAK,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACrH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAE3D,sFAAsF;AACtF,MAAM,WAAW,eAAe;IAC9B,yDAAyD;IACzD,IAAI,IAAI,WAAW,EAAE,CAAC;IACtB,+BAA+B;IAC/B,MAAM,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,CAAC;CACjC;AAED,+EAA+E;AAC/E,qBAAa,oBAAqB,YAAW,eAAe;IAC9C,OAAO,CAAC,QAAQ,CAAC,IAAI;gBAAJ,IAAI,EAAE,MAAM;IAEzC,IAAI,IAAI,WAAW,EAAE;IAqBrB,MAAM,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI;CAIhC;AAED;;;;GAIG;AACH,qBAAa,oBAAoB;IAC/B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAgB;IACxC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAkB;gBAElC,WAAW,EAAE,eAAe,EAAE,MAAM,GAAE,OAAO,CAAC,iBAAiB,CAAM;IAMjF,MAAM,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI;IAK/B,YAAY,CAAC,MAAM,EAAE,YAAY,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI;IAKpD,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,iBAAiB;IAIpC,IAAI,MAAM,IAAI,SAAS,WAAW,EAAE,CAEnC;CACF"}
@@ -0,0 +1,80 @@
1
+ /**
2
+ * parityMonitorStore.ts — durable persistence for the Phase-3 parity monitor.
3
+ *
4
+ * The zero-divergence window that gates cutover spans HOURS of real dual-forward traffic.
5
+ * If the monitor's passes lived only in memory, a server restart mid-window would silently
6
+ * reset the streak — at best re-blocking cutover (conservative, survivable), at worst making
7
+ * the gate's `windowMs` meaningless across the very restart it must survive. So passes are
8
+ * appended to a JSONL and reloaded on construction: the window is durable across restarts.
9
+ *
10
+ * Append-only JSONL (one MonitorPass per line) matches Instar's file-based-state convention
11
+ * and is crash-safe (a torn final line is skipped on reload, never corrupting the rest).
12
+ * The pure ParityMonitor stays the gate brain; this is the I/O shell around it.
13
+ */
14
+ import { appendFileSync, readFileSync, mkdirSync, existsSync } from 'node:fs';
15
+ import { dirname } from 'node:path';
16
+ import { ParityMonitor } from './parityMonitor.js';
17
+ /** The default JSONL-on-disk persistence (append-only, torn-line tolerant). */
18
+ export class JsonlPassPersistence {
19
+ path;
20
+ constructor(path) {
21
+ this.path = path;
22
+ }
23
+ load() {
24
+ if (!existsSync(this.path))
25
+ return [];
26
+ const out = [];
27
+ const lines = readFileSync(this.path, 'utf8').split('\n');
28
+ for (const line of lines) {
29
+ const trimmed = line.trim();
30
+ if (!trimmed)
31
+ continue;
32
+ try {
33
+ const p = JSON.parse(trimmed);
34
+ // Defensive: only accept well-formed pass records.
35
+ if (typeof p.at === 'string' && typeof p.clustersCompared === 'number' && typeof p.divergences === 'number') {
36
+ out.push({ at: p.at, clustersCompared: p.clustersCompared, divergences: p.divergences, divergent: !!p.divergent });
37
+ }
38
+ }
39
+ catch {
40
+ // A torn final line (crash mid-append) is skipped, never corrupting the prior window.
41
+ // @silent-fallback-ok — durability is best-effort-forward; a bad line is dropped, loudly recoverable.
42
+ }
43
+ }
44
+ return out;
45
+ }
46
+ append(pass) {
47
+ mkdirSync(dirname(this.path), { recursive: true });
48
+ appendFileSync(this.path, JSON.stringify(pass) + '\n');
49
+ }
50
+ }
51
+ /**
52
+ * A ParityMonitor whose passes survive a restart. Reloads persisted passes on construction,
53
+ * then every record() both persists and feeds the in-memory monitor. gate() delegates to the
54
+ * monitor over the full (reloaded + new) history.
55
+ */
56
+ export class DurableParityMonitor {
57
+ monitor;
58
+ persistence;
59
+ constructor(persistence, policy = {}) {
60
+ this.persistence = persistence;
61
+ this.monitor = new ParityMonitor(policy);
62
+ for (const p of persistence.load())
63
+ this.monitor.record(p);
64
+ }
65
+ record(pass) {
66
+ this.persistence.append(pass);
67
+ this.monitor.record(pass);
68
+ }
69
+ recordResult(result, at) {
70
+ const divergences = result.fingerprintDivergences.length + result.outcomeDivergences.length;
71
+ this.record({ at, clustersCompared: result.clustersCompared, divergences, divergent: divergences > 0 });
72
+ }
73
+ gate(now) {
74
+ return this.monitor.gate(now);
75
+ }
76
+ get passes() {
77
+ return this.monitor.passes;
78
+ }
79
+ }
80
+ //# sourceMappingURL=parityMonitorStore.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parityMonitorStore.js","sourceRoot":"","sources":["../../../src/feedback-factory/monitor/parityMonitorStore.ts"],"names":[],"mappings":"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"}