gitnexus 1.6.3-rc.14 → 1.6.3-rc.16
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/dist/_shared/graph/types.d.ts +16 -0
- package/dist/_shared/graph/types.d.ts.map +1 -1
- package/dist/core/ingestion/emit-references.d.ts +88 -0
- package/dist/core/ingestion/emit-references.js +229 -0
- package/dist/core/ingestion/shadow-harness.d.ts +113 -0
- package/dist/core/ingestion/shadow-harness.js +148 -0
- package/package.json +1 -1
|
@@ -61,5 +61,21 @@ export interface GraphRelationship {
|
|
|
61
61
|
confidence: number;
|
|
62
62
|
reason: string;
|
|
63
63
|
step?: number;
|
|
64
|
+
/**
|
|
65
|
+
* Per-signal evidence trace for edges emitted by the scope-based
|
|
66
|
+
* resolution pipeline (RFC #909 Ring 2 PKG #925). Populated by
|
|
67
|
+
* `emit-references.ts` when draining `ReferenceIndex` into the graph
|
|
68
|
+
* so downstream query / audit tools can inspect *why* a given edge
|
|
69
|
+
* was emitted with its confidence value.
|
|
70
|
+
*
|
|
71
|
+
* Optional and additive — every existing edge emitter ignores this
|
|
72
|
+
* field, and every existing query continues to work whether or not
|
|
73
|
+
* an edge carries it.
|
|
74
|
+
*/
|
|
75
|
+
evidence?: readonly {
|
|
76
|
+
readonly kind: string;
|
|
77
|
+
readonly weight: number;
|
|
78
|
+
readonly note?: string;
|
|
79
|
+
}[];
|
|
64
80
|
}
|
|
65
81
|
//# sourceMappingURL=types.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/graph/types.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/graph/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 5 of the RFC #909 ingestion lifecycle: drain `ReferenceIndex`
|
|
3
|
+
* into the knowledge graph as labeled edges with `confidence` and
|
|
4
|
+
* `evidence` properties (Ring 2 PKG #925).
|
|
5
|
+
*
|
|
6
|
+
* The resolution phase (future PR) writes `Reference` records into
|
|
7
|
+
* `model.scopes.referenceSites`-derived `ReferenceIndex`; this module
|
|
8
|
+
* materializes those records as `GraphRelationship`s via
|
|
9
|
+
* `graph.addRelationship`. Every emitted edge carries:
|
|
10
|
+
*
|
|
11
|
+
* - `type`: one of `'CALLS' | 'ACCESSES' | 'INHERITS' | 'USES'`
|
|
12
|
+
* (mapped from `Reference.kind` — `'read'` and `'write'` both route
|
|
13
|
+
* to `ACCESSES`; `'type-reference'` and `'import-use'` route to
|
|
14
|
+
* `USES`; `'call'` stays `CALLS`; `'inherits'` stays `INHERITS`).
|
|
15
|
+
* - `confidence`: the pre-computed confidence from the Reference record.
|
|
16
|
+
* - `reason`: human-readable summary (`"scope-resolution: call | confidence 0.75"`).
|
|
17
|
+
* - `evidence`: the full `ResolutionEvidence[]` trace — additive graph
|
|
18
|
+
* property (see `GraphRelationship.evidence` in gitnexus-shared),
|
|
19
|
+
* so queries that don't know about it are unaffected.
|
|
20
|
+
* - `step`: carries the reference's access-kind discriminant when
|
|
21
|
+
* available (`1` for read, `2` for write) so `ACCESSES` edges retain
|
|
22
|
+
* the read/write distinction without forcing a new edge type.
|
|
23
|
+
*
|
|
24
|
+
* ## Optional scope-tree flush
|
|
25
|
+
*
|
|
26
|
+
* When `INGESTION_EMIT_SCOPES=1` is set, this module also emits:
|
|
27
|
+
*
|
|
28
|
+
* - `Scope` nodes for every `Scope` in the tree
|
|
29
|
+
* - `CONTAINS` edges from parent scope to child scope
|
|
30
|
+
* - `DEFINES` edges from scope to its `ownedDefs` members
|
|
31
|
+
* - `IMPORTS` edges from scope to `targetModuleScope` of each finalized
|
|
32
|
+
* `ImportEdge` that carries one
|
|
33
|
+
*
|
|
34
|
+
* Off by default — existing queries that don't know about `Scope` nodes
|
|
35
|
+
* continue to work, and the storage cost is opt-in.
|
|
36
|
+
*
|
|
37
|
+
* ## Source-of-truth: the caller def for a reference
|
|
38
|
+
*
|
|
39
|
+
* A `Reference` says "some code inside `fromScope` references `toDef`".
|
|
40
|
+
* The graph wants `(callerNodeId, calleeNodeId)`. We resolve the caller
|
|
41
|
+
* by walking up the scope tree from `fromScope` until we find a scope
|
|
42
|
+
* whose `ownedDefs` contains a Function-like def. If no such ancestor
|
|
43
|
+
* exists, the edge is attributed to the first def owned by the innermost
|
|
44
|
+
* ancestor scope, and if THAT produces nothing either the edge is
|
|
45
|
+
* skipped (with a count returned in `EmitStats.skippedNoCaller`).
|
|
46
|
+
*/
|
|
47
|
+
import type { ReferenceIndex } from '../../_shared/index.js';
|
|
48
|
+
import type { KnowledgeGraph } from '../graph/types.js';
|
|
49
|
+
import type { ScopeResolutionIndexes } from './model/scope-resolution-indexes.js';
|
|
50
|
+
export interface EmitStats {
|
|
51
|
+
readonly edgesEmitted: number;
|
|
52
|
+
/** References dropped because no caller def could be resolved. */
|
|
53
|
+
readonly skippedNoCaller: number;
|
|
54
|
+
/** References dropped because `toDef` was not found in the DefIndex. */
|
|
55
|
+
readonly skippedMissingTarget: number;
|
|
56
|
+
/** Scope nodes emitted — `0` unless `INGESTION_EMIT_SCOPES=1`. */
|
|
57
|
+
readonly scopeNodesEmitted: number;
|
|
58
|
+
/** Scope-tree structural edges emitted — `0` unless `INGESTION_EMIT_SCOPES=1`. */
|
|
59
|
+
readonly scopeEdgesEmitted: number;
|
|
60
|
+
}
|
|
61
|
+
export interface EmitReferencesInput {
|
|
62
|
+
readonly graph: KnowledgeGraph;
|
|
63
|
+
readonly scopes: ScopeResolutionIndexes;
|
|
64
|
+
readonly referenceIndex: ReferenceIndex;
|
|
65
|
+
/** Human-consumable label for the `reason` prefix. Defaults to `'scope-resolution'`. */
|
|
66
|
+
readonly sourceLabel?: string;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Drain `referenceIndex.bySourceScope` into graph edges.
|
|
70
|
+
*
|
|
71
|
+
* The scope-tree flush is controlled separately by
|
|
72
|
+
* `INGESTION_EMIT_SCOPES` — callers can run `emitReferencesToGraph`
|
|
73
|
+
* without scope-node emission or layer the two calls as needed.
|
|
74
|
+
*/
|
|
75
|
+
export declare function emitReferencesToGraph(input: EmitReferencesInput): EmitStats;
|
|
76
|
+
/**
|
|
77
|
+
* Emit `Scope` nodes + `CONTAINS`/`DEFINES`/`IMPORTS` edges representing
|
|
78
|
+
* the lexical scope tree itself. Skipped unless `INGESTION_EMIT_SCOPES=1`
|
|
79
|
+
* at the public entry point; exported here for tests that want to
|
|
80
|
+
* exercise the path directly.
|
|
81
|
+
*/
|
|
82
|
+
export declare function emitScopeGraph(input: {
|
|
83
|
+
readonly graph: KnowledgeGraph;
|
|
84
|
+
readonly scopes: ScopeResolutionIndexes;
|
|
85
|
+
}): {
|
|
86
|
+
readonly scopeNodesEmitted: number;
|
|
87
|
+
readonly scopeEdgesEmitted: number;
|
|
88
|
+
};
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 5 of the RFC #909 ingestion lifecycle: drain `ReferenceIndex`
|
|
3
|
+
* into the knowledge graph as labeled edges with `confidence` and
|
|
4
|
+
* `evidence` properties (Ring 2 PKG #925).
|
|
5
|
+
*
|
|
6
|
+
* The resolution phase (future PR) writes `Reference` records into
|
|
7
|
+
* `model.scopes.referenceSites`-derived `ReferenceIndex`; this module
|
|
8
|
+
* materializes those records as `GraphRelationship`s via
|
|
9
|
+
* `graph.addRelationship`. Every emitted edge carries:
|
|
10
|
+
*
|
|
11
|
+
* - `type`: one of `'CALLS' | 'ACCESSES' | 'INHERITS' | 'USES'`
|
|
12
|
+
* (mapped from `Reference.kind` — `'read'` and `'write'` both route
|
|
13
|
+
* to `ACCESSES`; `'type-reference'` and `'import-use'` route to
|
|
14
|
+
* `USES`; `'call'` stays `CALLS`; `'inherits'` stays `INHERITS`).
|
|
15
|
+
* - `confidence`: the pre-computed confidence from the Reference record.
|
|
16
|
+
* - `reason`: human-readable summary (`"scope-resolution: call | confidence 0.75"`).
|
|
17
|
+
* - `evidence`: the full `ResolutionEvidence[]` trace — additive graph
|
|
18
|
+
* property (see `GraphRelationship.evidence` in gitnexus-shared),
|
|
19
|
+
* so queries that don't know about it are unaffected.
|
|
20
|
+
* - `step`: carries the reference's access-kind discriminant when
|
|
21
|
+
* available (`1` for read, `2` for write) so `ACCESSES` edges retain
|
|
22
|
+
* the read/write distinction without forcing a new edge type.
|
|
23
|
+
*
|
|
24
|
+
* ## Optional scope-tree flush
|
|
25
|
+
*
|
|
26
|
+
* When `INGESTION_EMIT_SCOPES=1` is set, this module also emits:
|
|
27
|
+
*
|
|
28
|
+
* - `Scope` nodes for every `Scope` in the tree
|
|
29
|
+
* - `CONTAINS` edges from parent scope to child scope
|
|
30
|
+
* - `DEFINES` edges from scope to its `ownedDefs` members
|
|
31
|
+
* - `IMPORTS` edges from scope to `targetModuleScope` of each finalized
|
|
32
|
+
* `ImportEdge` that carries one
|
|
33
|
+
*
|
|
34
|
+
* Off by default — existing queries that don't know about `Scope` nodes
|
|
35
|
+
* continue to work, and the storage cost is opt-in.
|
|
36
|
+
*
|
|
37
|
+
* ## Source-of-truth: the caller def for a reference
|
|
38
|
+
*
|
|
39
|
+
* A `Reference` says "some code inside `fromScope` references `toDef`".
|
|
40
|
+
* The graph wants `(callerNodeId, calleeNodeId)`. We resolve the caller
|
|
41
|
+
* by walking up the scope tree from `fromScope` until we find a scope
|
|
42
|
+
* whose `ownedDefs` contains a Function-like def. If no such ancestor
|
|
43
|
+
* exists, the edge is attributed to the first def owned by the innermost
|
|
44
|
+
* ancestor scope, and if THAT produces nothing either the edge is
|
|
45
|
+
* skipped (with a count returned in `EmitStats.skippedNoCaller`).
|
|
46
|
+
*/
|
|
47
|
+
/**
|
|
48
|
+
* Drain `referenceIndex.bySourceScope` into graph edges.
|
|
49
|
+
*
|
|
50
|
+
* The scope-tree flush is controlled separately by
|
|
51
|
+
* `INGESTION_EMIT_SCOPES` — callers can run `emitReferencesToGraph`
|
|
52
|
+
* without scope-node emission or layer the two calls as needed.
|
|
53
|
+
*/
|
|
54
|
+
export function emitReferencesToGraph(input) {
|
|
55
|
+
const { graph, scopes, referenceIndex } = input;
|
|
56
|
+
const sourceLabel = input.sourceLabel ?? 'scope-resolution';
|
|
57
|
+
let edgesEmitted = 0;
|
|
58
|
+
let skippedNoCaller = 0;
|
|
59
|
+
let skippedMissingTarget = 0;
|
|
60
|
+
for (const [fromScope, refs] of referenceIndex.bySourceScope) {
|
|
61
|
+
for (const ref of refs) {
|
|
62
|
+
const targetDef = scopes.defs.get(ref.toDef);
|
|
63
|
+
if (targetDef === undefined) {
|
|
64
|
+
skippedMissingTarget++;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const callerId = resolveCallerNodeId(fromScope, scopes);
|
|
68
|
+
if (callerId === undefined) {
|
|
69
|
+
skippedNoCaller++;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
graph.addRelationship(buildRelationship(ref, callerId, targetDef, sourceLabel));
|
|
73
|
+
edgesEmitted++;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const scopeStats = isScopeEmissionEnabled()
|
|
77
|
+
? emitScopeGraph({ graph, scopes })
|
|
78
|
+
: { scopeNodesEmitted: 0, scopeEdgesEmitted: 0 };
|
|
79
|
+
return { edgesEmitted, skippedNoCaller, skippedMissingTarget, ...scopeStats };
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Emit `Scope` nodes + `CONTAINS`/`DEFINES`/`IMPORTS` edges representing
|
|
83
|
+
* the lexical scope tree itself. Skipped unless `INGESTION_EMIT_SCOPES=1`
|
|
84
|
+
* at the public entry point; exported here for tests that want to
|
|
85
|
+
* exercise the path directly.
|
|
86
|
+
*/
|
|
87
|
+
export function emitScopeGraph(input) {
|
|
88
|
+
const { graph, scopes } = input;
|
|
89
|
+
let scopeNodesEmitted = 0;
|
|
90
|
+
let scopeEdgesEmitted = 0;
|
|
91
|
+
for (const scope of scopes.scopeTree.byId.values()) {
|
|
92
|
+
graph.addNode({
|
|
93
|
+
id: scope.id,
|
|
94
|
+
label: 'CodeElement', // the generic bucket for non-symbol graph nodes
|
|
95
|
+
properties: {
|
|
96
|
+
name: scope.kind,
|
|
97
|
+
filePath: scope.filePath,
|
|
98
|
+
startLine: scope.range.startLine,
|
|
99
|
+
endLine: scope.range.endLine,
|
|
100
|
+
description: `Scope: ${scope.kind}`,
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
scopeNodesEmitted++;
|
|
104
|
+
if (scope.parent !== null) {
|
|
105
|
+
graph.addRelationship({
|
|
106
|
+
id: `rel:contains:${scope.parent}->${scope.id}`,
|
|
107
|
+
sourceId: scope.parent,
|
|
108
|
+
targetId: scope.id,
|
|
109
|
+
type: 'CONTAINS',
|
|
110
|
+
confidence: 1,
|
|
111
|
+
reason: 'scope-tree parent/child',
|
|
112
|
+
});
|
|
113
|
+
scopeEdgesEmitted++;
|
|
114
|
+
}
|
|
115
|
+
for (const def of scope.ownedDefs) {
|
|
116
|
+
graph.addRelationship({
|
|
117
|
+
id: `rel:defines:${scope.id}->${def.nodeId}`,
|
|
118
|
+
sourceId: scope.id,
|
|
119
|
+
targetId: def.nodeId,
|
|
120
|
+
type: 'DEFINES',
|
|
121
|
+
confidence: 1,
|
|
122
|
+
reason: 'scope.ownedDefs',
|
|
123
|
+
});
|
|
124
|
+
scopeEdgesEmitted++;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
for (const [scopeId, edges] of scopes.imports) {
|
|
128
|
+
for (const edge of edges) {
|
|
129
|
+
if (edge.targetModuleScope === undefined)
|
|
130
|
+
continue;
|
|
131
|
+
graph.addRelationship({
|
|
132
|
+
id: `rel:imports:${scopeId}->${edge.targetModuleScope}:${edge.localName}`,
|
|
133
|
+
sourceId: scopeId,
|
|
134
|
+
targetId: edge.targetModuleScope,
|
|
135
|
+
type: 'IMPORTS',
|
|
136
|
+
confidence: edge.linkStatus === 'unresolved' ? 0.5 : 1,
|
|
137
|
+
reason: `import ${edge.kind} ${edge.localName}`,
|
|
138
|
+
});
|
|
139
|
+
scopeEdgesEmitted++;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return { scopeNodesEmitted, scopeEdgesEmitted };
|
|
143
|
+
}
|
|
144
|
+
// ─── Internal ───────────────────────────────────────────────────────────────
|
|
145
|
+
/** Accepted truthy values for `INGESTION_EMIT_SCOPES`. */
|
|
146
|
+
const TRUTHY = new Set(['true', '1', 'yes']);
|
|
147
|
+
function isScopeEmissionEnabled() {
|
|
148
|
+
const raw = process.env['INGESTION_EMIT_SCOPES'];
|
|
149
|
+
if (raw === undefined)
|
|
150
|
+
return false;
|
|
151
|
+
return TRUTHY.has(raw.trim().toLowerCase());
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Walk up from `startScope` looking for the first ancestor scope whose
|
|
155
|
+
* `ownedDefs` contains a Function-like def (Function / Method /
|
|
156
|
+
* Constructor). Fall back to the innermost ancestor's first `ownedDef`
|
|
157
|
+
* if none is found; return `undefined` if all ancestors have no defs.
|
|
158
|
+
*/
|
|
159
|
+
function resolveCallerNodeId(startScope, scopes) {
|
|
160
|
+
const tree = scopes.scopeTree;
|
|
161
|
+
let current = startScope;
|
|
162
|
+
const visited = new Set();
|
|
163
|
+
let firstOwnedFallback;
|
|
164
|
+
while (current !== null) {
|
|
165
|
+
if (visited.has(current))
|
|
166
|
+
break;
|
|
167
|
+
visited.add(current);
|
|
168
|
+
const scope = tree.getScope(current);
|
|
169
|
+
if (scope === undefined)
|
|
170
|
+
break;
|
|
171
|
+
// Prefer a Function-like owner.
|
|
172
|
+
const fnDef = scope.ownedDefs.find((d) => isFunctionLike(d.type));
|
|
173
|
+
if (fnDef !== undefined)
|
|
174
|
+
return fnDef.nodeId;
|
|
175
|
+
// Stash the first owned def we see as a conservative fallback.
|
|
176
|
+
if (firstOwnedFallback === undefined && scope.ownedDefs.length > 0) {
|
|
177
|
+
firstOwnedFallback = scope.ownedDefs[0].nodeId;
|
|
178
|
+
}
|
|
179
|
+
current = scope.parent;
|
|
180
|
+
}
|
|
181
|
+
return firstOwnedFallback;
|
|
182
|
+
}
|
|
183
|
+
function isFunctionLike(type) {
|
|
184
|
+
return type === 'Function' || type === 'Method' || type === 'Constructor';
|
|
185
|
+
}
|
|
186
|
+
function buildRelationship(ref, callerId, targetDef, sourceLabel) {
|
|
187
|
+
const type = mapKindToType(ref.kind);
|
|
188
|
+
const reason = `${sourceLabel}: ${ref.kind} | confidence ${ref.confidence.toFixed(3)}`;
|
|
189
|
+
// `step` encodes read/write discriminator for ACCESSES edges (1=read, 2=write).
|
|
190
|
+
// Other kinds omit `step`.
|
|
191
|
+
const step = ref.kind === 'read' ? 1 : ref.kind === 'write' ? 2 : undefined;
|
|
192
|
+
return {
|
|
193
|
+
id: `rel:${type}:${callerId}->${targetDef.nodeId}:${ref.atRange.startLine}:${ref.atRange.startCol}`,
|
|
194
|
+
sourceId: callerId,
|
|
195
|
+
targetId: targetDef.nodeId,
|
|
196
|
+
type,
|
|
197
|
+
confidence: ref.confidence,
|
|
198
|
+
reason,
|
|
199
|
+
evidence: ref.evidence.map(serializeEvidence),
|
|
200
|
+
...(step !== undefined ? { step } : {}),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Map a `Reference.kind` to an existing `RelationshipType`. Read/write
|
|
205
|
+
* both fold into `ACCESSES`; `type-reference` + `import-use` both fold
|
|
206
|
+
* into `USES`. This keeps the graph schema additive — no new
|
|
207
|
+
* RelationshipType values are introduced by this module.
|
|
208
|
+
*/
|
|
209
|
+
function mapKindToType(kind) {
|
|
210
|
+
switch (kind) {
|
|
211
|
+
case 'call':
|
|
212
|
+
return 'CALLS';
|
|
213
|
+
case 'read':
|
|
214
|
+
case 'write':
|
|
215
|
+
return 'ACCESSES';
|
|
216
|
+
case 'inherits':
|
|
217
|
+
return 'INHERITS';
|
|
218
|
+
case 'type-reference':
|
|
219
|
+
case 'import-use':
|
|
220
|
+
return 'USES';
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
function serializeEvidence(e) {
|
|
224
|
+
return {
|
|
225
|
+
kind: e.kind,
|
|
226
|
+
weight: e.weight,
|
|
227
|
+
...(e.note !== undefined ? { note: e.note } : {}),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shadow-mode parity harness — dual-run observability for the RFC #909
|
|
3
|
+
* registry rollout (RFC §6.3; Ring 2 PKG #923).
|
|
4
|
+
*
|
|
5
|
+
* ## What it does
|
|
6
|
+
*
|
|
7
|
+
* - Exposes `record({ language, callsite, legacy, newResult })` for
|
|
8
|
+
* every call site where the caller has BOTH a legacy-DAG resolution
|
|
9
|
+
* and a new `Registry.lookup` resolution.
|
|
10
|
+
* - Computes a `ShadowDiff` per record via shared `diffResolutions`
|
|
11
|
+
* (#918) and accumulates them in a per-language bucket.
|
|
12
|
+
* - At the end of a run, aggregates into a `ShadowParityReport` via
|
|
13
|
+
* shared `aggregateDiffs` (#918) — per-language parity %,
|
|
14
|
+
* evidence-kind breakdown of divergences, grand-total overall row.
|
|
15
|
+
* - Optionally persists the report as JSON under
|
|
16
|
+
* `.gitnexus/shadow-parity/` so the static dashboard at
|
|
17
|
+
* `gitnexus/shadow-parity-dashboard/` can render it offline.
|
|
18
|
+
*
|
|
19
|
+
* ## What it does NOT do
|
|
20
|
+
*
|
|
21
|
+
* - **Invoke either resolution path itself.** The caller must run
|
|
22
|
+
* legacy + `Registry.lookup` and pass results in. The harness is a
|
|
23
|
+
* side-car, not a dispatcher — this keeps call-processor integration
|
|
24
|
+
* surgical when it lands (tracked as a follow-up; the shared model
|
|
25
|
+
* doesn't dual-invoke on its own).
|
|
26
|
+
* - **Flip anything.** `REGISTRY_PRIMARY_<LANG>` lives in
|
|
27
|
+
* `registry-primary-flag.ts` (#924); the harness records the
|
|
28
|
+
* caller-supplied "which side is primary" bit for each record so the
|
|
29
|
+
* dashboard can label rows, but it does not consult the flag itself.
|
|
30
|
+
*
|
|
31
|
+
* ## Activation
|
|
32
|
+
*
|
|
33
|
+
* `GITNEXUS_SHADOW_MODE=1` (or `'true'`, `'yes'`, case-insensitive,
|
|
34
|
+
* trimmed) enables the harness. When disabled, `record()` is a cheap
|
|
35
|
+
* no-op: no accumulation, no allocation beyond the harness object
|
|
36
|
+
* itself. Callers can always construct a harness and hand it through;
|
|
37
|
+
* the "off" overhead is near-zero.
|
|
38
|
+
*
|
|
39
|
+
* ## Persistence shape
|
|
40
|
+
*
|
|
41
|
+
* When `persist()` is called, the harness writes TWO files:
|
|
42
|
+
*
|
|
43
|
+
* - `<outputDir>/<runId>.json` — the timestamped snapshot (immutable)
|
|
44
|
+
* - `<outputDir>/latest.json` — a pointer that the dashboard reads
|
|
45
|
+
*
|
|
46
|
+
* Both files contain the same `PersistedShadowReport` payload:
|
|
47
|
+
*
|
|
48
|
+
* {
|
|
49
|
+
* schemaVersion: 1,
|
|
50
|
+
* runId: "<iso-8601>-<rand>",
|
|
51
|
+
* generatedAt: "<iso-8601>",
|
|
52
|
+
* primaryByLanguage: { [lang]: "legacy" | "registry" },
|
|
53
|
+
* report: <ShadowParityReport>
|
|
54
|
+
* }
|
|
55
|
+
*
|
|
56
|
+
* Schema-version-gated so future format changes don't silently confuse
|
|
57
|
+
* older dashboards. The dashboard renders `report.perLanguage` rows and
|
|
58
|
+
* annotates each with `primaryByLanguage[lang]`.
|
|
59
|
+
*/
|
|
60
|
+
import { type Resolution, type ShadowCallsite, type ShadowParityReport, type SupportedLanguages } from '../../_shared/index.js';
|
|
61
|
+
/** Which side of the dual-run is considered authoritative for this language. */
|
|
62
|
+
export type PrimarySide = 'legacy' | 'registry';
|
|
63
|
+
/** One record per call site the caller dual-runs. */
|
|
64
|
+
export interface ShadowRecordInput {
|
|
65
|
+
readonly language: SupportedLanguages;
|
|
66
|
+
readonly callsite: ShadowCallsite;
|
|
67
|
+
readonly legacy: readonly Resolution[];
|
|
68
|
+
readonly newResult: readonly Resolution[];
|
|
69
|
+
/**
|
|
70
|
+
* Which side drove the actual runtime answer for this record. Lets the
|
|
71
|
+
* dashboard distinguish "registry-primary, legacy is shadow" from the
|
|
72
|
+
* default "legacy-primary, registry is shadow" without re-reading
|
|
73
|
+
* `REGISTRY_PRIMARY_<LANG>` env vars at render time.
|
|
74
|
+
*/
|
|
75
|
+
readonly primary: PrimarySide;
|
|
76
|
+
}
|
|
77
|
+
/** Persisted JSON shape. Schema-versioned for future migrations. */
|
|
78
|
+
export interface PersistedShadowReport {
|
|
79
|
+
readonly schemaVersion: 1;
|
|
80
|
+
readonly runId: string;
|
|
81
|
+
readonly generatedAt: string;
|
|
82
|
+
readonly primaryByLanguage: Readonly<Partial<Record<SupportedLanguages, PrimarySide>>>;
|
|
83
|
+
readonly report: ShadowParityReport;
|
|
84
|
+
}
|
|
85
|
+
export interface ShadowHarness {
|
|
86
|
+
/** `true` iff `GITNEXUS_SHADOW_MODE` is truthy. When `false`, `record()` is a no-op. */
|
|
87
|
+
readonly enabled: boolean;
|
|
88
|
+
/** Accumulate a dual-run observation. No-op when `enabled === false`. */
|
|
89
|
+
record(input: ShadowRecordInput): void;
|
|
90
|
+
/** Number of records accumulated so far. Useful for diagnostics / tests. */
|
|
91
|
+
size(): number;
|
|
92
|
+
/**
|
|
93
|
+
* Aggregate the accumulated records into a `ShadowParityReport`
|
|
94
|
+
* without persisting. Returns a deterministic snapshot each call;
|
|
95
|
+
* idempotent with respect to `record()` ordering.
|
|
96
|
+
*/
|
|
97
|
+
snapshot(now?: Date): ShadowParityReport;
|
|
98
|
+
/**
|
|
99
|
+
* Write the aggregated snapshot to JSON. Resolves to the path of the
|
|
100
|
+
* per-run file. Also writes/overwrites `latest.json` alongside.
|
|
101
|
+
*
|
|
102
|
+
* Creates `outputDir` if it doesn't exist.
|
|
103
|
+
*/
|
|
104
|
+
persist(outputDir: string, now?: Date): Promise<string>;
|
|
105
|
+
/** Reset the accumulator. Preserves `enabled`. */
|
|
106
|
+
clear(): void;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Construct a harness. Reads `GITNEXUS_SHADOW_MODE` at construction time
|
|
110
|
+
* (not per-`record()` call) so repeated no-op records don't re-check the
|
|
111
|
+
* env var in the hot path.
|
|
112
|
+
*/
|
|
113
|
+
export declare function createShadowHarness(): ShadowHarness;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shadow-mode parity harness — dual-run observability for the RFC #909
|
|
3
|
+
* registry rollout (RFC §6.3; Ring 2 PKG #923).
|
|
4
|
+
*
|
|
5
|
+
* ## What it does
|
|
6
|
+
*
|
|
7
|
+
* - Exposes `record({ language, callsite, legacy, newResult })` for
|
|
8
|
+
* every call site where the caller has BOTH a legacy-DAG resolution
|
|
9
|
+
* and a new `Registry.lookup` resolution.
|
|
10
|
+
* - Computes a `ShadowDiff` per record via shared `diffResolutions`
|
|
11
|
+
* (#918) and accumulates them in a per-language bucket.
|
|
12
|
+
* - At the end of a run, aggregates into a `ShadowParityReport` via
|
|
13
|
+
* shared `aggregateDiffs` (#918) — per-language parity %,
|
|
14
|
+
* evidence-kind breakdown of divergences, grand-total overall row.
|
|
15
|
+
* - Optionally persists the report as JSON under
|
|
16
|
+
* `.gitnexus/shadow-parity/` so the static dashboard at
|
|
17
|
+
* `gitnexus/shadow-parity-dashboard/` can render it offline.
|
|
18
|
+
*
|
|
19
|
+
* ## What it does NOT do
|
|
20
|
+
*
|
|
21
|
+
* - **Invoke either resolution path itself.** The caller must run
|
|
22
|
+
* legacy + `Registry.lookup` and pass results in. The harness is a
|
|
23
|
+
* side-car, not a dispatcher — this keeps call-processor integration
|
|
24
|
+
* surgical when it lands (tracked as a follow-up; the shared model
|
|
25
|
+
* doesn't dual-invoke on its own).
|
|
26
|
+
* - **Flip anything.** `REGISTRY_PRIMARY_<LANG>` lives in
|
|
27
|
+
* `registry-primary-flag.ts` (#924); the harness records the
|
|
28
|
+
* caller-supplied "which side is primary" bit for each record so the
|
|
29
|
+
* dashboard can label rows, but it does not consult the flag itself.
|
|
30
|
+
*
|
|
31
|
+
* ## Activation
|
|
32
|
+
*
|
|
33
|
+
* `GITNEXUS_SHADOW_MODE=1` (or `'true'`, `'yes'`, case-insensitive,
|
|
34
|
+
* trimmed) enables the harness. When disabled, `record()` is a cheap
|
|
35
|
+
* no-op: no accumulation, no allocation beyond the harness object
|
|
36
|
+
* itself. Callers can always construct a harness and hand it through;
|
|
37
|
+
* the "off" overhead is near-zero.
|
|
38
|
+
*
|
|
39
|
+
* ## Persistence shape
|
|
40
|
+
*
|
|
41
|
+
* When `persist()` is called, the harness writes TWO files:
|
|
42
|
+
*
|
|
43
|
+
* - `<outputDir>/<runId>.json` — the timestamped snapshot (immutable)
|
|
44
|
+
* - `<outputDir>/latest.json` — a pointer that the dashboard reads
|
|
45
|
+
*
|
|
46
|
+
* Both files contain the same `PersistedShadowReport` payload:
|
|
47
|
+
*
|
|
48
|
+
* {
|
|
49
|
+
* schemaVersion: 1,
|
|
50
|
+
* runId: "<iso-8601>-<rand>",
|
|
51
|
+
* generatedAt: "<iso-8601>",
|
|
52
|
+
* primaryByLanguage: { [lang]: "legacy" | "registry" },
|
|
53
|
+
* report: <ShadowParityReport>
|
|
54
|
+
* }
|
|
55
|
+
*
|
|
56
|
+
* Schema-version-gated so future format changes don't silently confuse
|
|
57
|
+
* older dashboards. The dashboard renders `report.perLanguage` rows and
|
|
58
|
+
* annotates each with `primaryByLanguage[lang]`.
|
|
59
|
+
*/
|
|
60
|
+
import * as fs from 'node:fs/promises';
|
|
61
|
+
import * as path from 'node:path';
|
|
62
|
+
import { aggregateDiffs, diffResolutions, } from '../../_shared/index.js';
|
|
63
|
+
/**
|
|
64
|
+
* Construct a harness. Reads `GITNEXUS_SHADOW_MODE` at construction time
|
|
65
|
+
* (not per-`record()` call) so repeated no-op records don't re-check the
|
|
66
|
+
* env var in the hot path.
|
|
67
|
+
*/
|
|
68
|
+
export function createShadowHarness() {
|
|
69
|
+
const enabled = parseShadowModeEnv(process.env['GITNEXUS_SHADOW_MODE']);
|
|
70
|
+
const records = [];
|
|
71
|
+
const primaryByLanguage = {};
|
|
72
|
+
const recordImpl = (input) => {
|
|
73
|
+
if (!enabled)
|
|
74
|
+
return;
|
|
75
|
+
const diff = diffResolutions(input.callsite, input.legacy, input.newResult);
|
|
76
|
+
records.push({ language: input.language, diff });
|
|
77
|
+
// Primary per-language is resolved by last-write. In practice a run
|
|
78
|
+
// is single-threaded with respect to flag readings, so this is
|
|
79
|
+
// deterministic; a language's primary cannot change mid-run.
|
|
80
|
+
primaryByLanguage[input.language] = input.primary;
|
|
81
|
+
};
|
|
82
|
+
const snapshotImpl = (now = new Date()) => {
|
|
83
|
+
return aggregateDiffs(records, now);
|
|
84
|
+
};
|
|
85
|
+
const persistImpl = async (outputDir, now = new Date()) => {
|
|
86
|
+
await fs.mkdir(outputDir, { recursive: true });
|
|
87
|
+
const report = snapshotImpl(now);
|
|
88
|
+
const runId = makeRunId(now);
|
|
89
|
+
const payload = {
|
|
90
|
+
schemaVersion: 1,
|
|
91
|
+
runId,
|
|
92
|
+
generatedAt: now.toISOString(),
|
|
93
|
+
primaryByLanguage,
|
|
94
|
+
report,
|
|
95
|
+
};
|
|
96
|
+
const json = JSON.stringify(payload, null, 2);
|
|
97
|
+
const perRunPath = path.join(outputDir, `${runId}.json`);
|
|
98
|
+
const latestPath = path.join(outputDir, 'latest.json');
|
|
99
|
+
await fs.writeFile(perRunPath, json, 'utf8');
|
|
100
|
+
await fs.writeFile(latestPath, json, 'utf8');
|
|
101
|
+
return perRunPath;
|
|
102
|
+
};
|
|
103
|
+
const clearImpl = () => {
|
|
104
|
+
records.length = 0;
|
|
105
|
+
for (const key of Object.keys(primaryByLanguage)) {
|
|
106
|
+
delete primaryByLanguage[key];
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
return {
|
|
110
|
+
enabled,
|
|
111
|
+
record: recordImpl,
|
|
112
|
+
size: () => records.length,
|
|
113
|
+
snapshot: snapshotImpl,
|
|
114
|
+
persist: persistImpl,
|
|
115
|
+
clear: clearImpl,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
// ─── Internal helpers ─────────────────────────────────────────────────────
|
|
119
|
+
/**
|
|
120
|
+
* Env-var parser for `GITNEXUS_SHADOW_MODE`. Accepts the same truthy
|
|
121
|
+
* conventions as `REGISTRY_PRIMARY_<LANG>` from #924: `'true'` / `'1'` /
|
|
122
|
+
* `'yes'`, case-insensitive, whitespace-trimmed. Anything else — including
|
|
123
|
+
* `undefined`, `''`, `'false'`, `'off'`, typos — is false.
|
|
124
|
+
*/
|
|
125
|
+
function parseShadowModeEnv(raw) {
|
|
126
|
+
if (raw === undefined)
|
|
127
|
+
return false;
|
|
128
|
+
const normalized = raw.trim().toLowerCase();
|
|
129
|
+
return normalized === 'true' || normalized === '1' || normalized === 'yes';
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Deterministic run id derived from the timestamp plus 4 random bytes
|
|
133
|
+
* of entropy. The timestamp comes first so files sort chronologically;
|
|
134
|
+
* the entropy suffix prevents collisions when multiple runs share a
|
|
135
|
+
* clock-second. Shape: `YYYYMMDD-HHMMSS-xxxxxxxx`.
|
|
136
|
+
*/
|
|
137
|
+
function makeRunId(now) {
|
|
138
|
+
const y = now.getUTCFullYear().toString().padStart(4, '0');
|
|
139
|
+
const m = (now.getUTCMonth() + 1).toString().padStart(2, '0');
|
|
140
|
+
const d = now.getUTCDate().toString().padStart(2, '0');
|
|
141
|
+
const h = now.getUTCHours().toString().padStart(2, '0');
|
|
142
|
+
const min = now.getUTCMinutes().toString().padStart(2, '0');
|
|
143
|
+
const s = now.getUTCSeconds().toString().padStart(2, '0');
|
|
144
|
+
const entropy = Math.floor(Math.random() * 0xffffffff)
|
|
145
|
+
.toString(16)
|
|
146
|
+
.padStart(8, '0');
|
|
147
|
+
return `${y}${m}${d}-${h}${min}${s}-${entropy}`;
|
|
148
|
+
}
|
package/package.json
CHANGED