gitnexus 1.6.3-rc.2 → 1.6.3-rc.21
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/_shared/index.d.ts +41 -1
- package/dist/_shared/index.d.ts.map +1 -1
- package/dist/_shared/index.js +28 -0
- package/dist/_shared/index.js.map +1 -1
- package/dist/_shared/scope-resolution/def-index.d.ts +36 -0
- package/dist/_shared/scope-resolution/def-index.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/def-index.js +51 -0
- package/dist/_shared/scope-resolution/def-index.js.map +1 -0
- package/dist/_shared/scope-resolution/finalize-algorithm.d.ts +139 -0
- package/dist/_shared/scope-resolution/finalize-algorithm.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/finalize-algorithm.js +479 -0
- package/dist/_shared/scope-resolution/finalize-algorithm.js.map +1 -0
- package/dist/_shared/scope-resolution/method-dispatch-index.d.ts +80 -0
- package/dist/_shared/scope-resolution/method-dispatch-index.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/method-dispatch-index.js +79 -0
- package/dist/_shared/scope-resolution/method-dispatch-index.js.map +1 -0
- package/dist/_shared/scope-resolution/module-scope-index.d.ts +46 -0
- package/dist/_shared/scope-resolution/module-scope-index.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/module-scope-index.js +58 -0
- package/dist/_shared/scope-resolution/module-scope-index.js.map +1 -0
- package/dist/_shared/scope-resolution/parsed-file.d.ts +64 -0
- package/dist/_shared/scope-resolution/parsed-file.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/parsed-file.js +42 -0
- package/dist/_shared/scope-resolution/parsed-file.js.map +1 -0
- package/dist/_shared/scope-resolution/position-index.d.ts +62 -0
- package/dist/_shared/scope-resolution/position-index.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/position-index.js +134 -0
- package/dist/_shared/scope-resolution/position-index.js.map +1 -0
- package/dist/_shared/scope-resolution/qualified-name-index.d.ts +44 -0
- package/dist/_shared/scope-resolution/qualified-name-index.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/qualified-name-index.js +75 -0
- package/dist/_shared/scope-resolution/qualified-name-index.js.map +1 -0
- package/dist/_shared/scope-resolution/reference-site.d.ts +67 -0
- package/dist/_shared/scope-resolution/reference-site.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/reference-site.js +24 -0
- package/dist/_shared/scope-resolution/reference-site.js.map +1 -0
- package/dist/_shared/scope-resolution/registries/class-registry.d.ts +27 -0
- package/dist/_shared/scope-resolution/registries/class-registry.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/registries/class-registry.js +30 -0
- package/dist/_shared/scope-resolution/registries/class-registry.js.map +1 -0
- package/dist/_shared/scope-resolution/registries/context.d.ts +69 -0
- package/dist/_shared/scope-resolution/registries/context.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/registries/context.js +44 -0
- package/dist/_shared/scope-resolution/registries/context.js.map +1 -0
- package/dist/_shared/scope-resolution/registries/evidence.d.ts +56 -0
- package/dist/_shared/scope-resolution/registries/evidence.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/registries/evidence.js +150 -0
- package/dist/_shared/scope-resolution/registries/evidence.js.map +1 -0
- package/dist/_shared/scope-resolution/registries/field-registry.d.ts +26 -0
- package/dist/_shared/scope-resolution/registries/field-registry.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/registries/field-registry.js +31 -0
- package/dist/_shared/scope-resolution/registries/field-registry.js.map +1 -0
- package/dist/_shared/scope-resolution/registries/lookup-core.d.ts +81 -0
- package/dist/_shared/scope-resolution/registries/lookup-core.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/registries/lookup-core.js +332 -0
- package/dist/_shared/scope-resolution/registries/lookup-core.js.map +1 -0
- package/dist/_shared/scope-resolution/registries/lookup-qualified.d.ts +33 -0
- package/dist/_shared/scope-resolution/registries/lookup-qualified.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/registries/lookup-qualified.js +56 -0
- package/dist/_shared/scope-resolution/registries/lookup-qualified.js.map +1 -0
- package/dist/_shared/scope-resolution/registries/method-registry.d.ts +36 -0
- package/dist/_shared/scope-resolution/registries/method-registry.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/registries/method-registry.js +32 -0
- package/dist/_shared/scope-resolution/registries/method-registry.js.map +1 -0
- package/dist/_shared/scope-resolution/registries/tie-breaks.d.ts +43 -0
- package/dist/_shared/scope-resolution/registries/tie-breaks.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/registries/tie-breaks.js +60 -0
- package/dist/_shared/scope-resolution/registries/tie-breaks.js.map +1 -0
- package/dist/_shared/scope-resolution/resolve-type-ref.d.ts +53 -0
- package/dist/_shared/scope-resolution/resolve-type-ref.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/resolve-type-ref.js +126 -0
- package/dist/_shared/scope-resolution/resolve-type-ref.js.map +1 -0
- package/dist/_shared/scope-resolution/scope-id.d.ts +43 -0
- package/dist/_shared/scope-resolution/scope-id.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/scope-id.js +46 -0
- package/dist/_shared/scope-resolution/scope-id.js.map +1 -0
- package/dist/_shared/scope-resolution/scope-tree.d.ts +61 -0
- package/dist/_shared/scope-resolution/scope-tree.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/scope-tree.js +186 -0
- package/dist/_shared/scope-resolution/scope-tree.js.map +1 -0
- package/dist/_shared/scope-resolution/shadow/aggregate.d.ts +63 -0
- package/dist/_shared/scope-resolution/shadow/aggregate.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/shadow/aggregate.js +122 -0
- package/dist/_shared/scope-resolution/shadow/aggregate.js.map +1 -0
- package/dist/_shared/scope-resolution/shadow/diff.d.ts +59 -0
- package/dist/_shared/scope-resolution/shadow/diff.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/shadow/diff.js +79 -0
- package/dist/_shared/scope-resolution/shadow/diff.js.map +1 -0
- package/dist/_shared/scope-resolution/types.d.ts +156 -0
- package/dist/_shared/scope-resolution/types.d.ts.map +1 -1
- package/dist/cli/analyze.d.ts +15 -0
- package/dist/cli/analyze.js +22 -1
- package/dist/cli/index.js +4 -0
- package/dist/cli/list.js +11 -1
- package/dist/core/ingestion/emit-references.d.ts +88 -0
- package/dist/core/ingestion/emit-references.js +229 -0
- package/dist/core/ingestion/finalize-orchestrator.d.ts +63 -0
- package/dist/core/ingestion/finalize-orchestrator.js +139 -0
- package/dist/core/ingestion/framework-detection.js +6 -2
- package/dist/core/ingestion/import-target-adapter.d.ts +73 -0
- package/dist/core/ingestion/import-target-adapter.js +95 -0
- package/dist/core/ingestion/language-provider.d.ts +187 -1
- package/dist/core/ingestion/model/scope-resolution-indexes.d.ts +59 -0
- package/dist/core/ingestion/model/scope-resolution-indexes.js +42 -0
- package/dist/core/ingestion/model/semantic-model.d.ts +25 -0
- package/dist/core/ingestion/model/semantic-model.js +16 -0
- package/dist/core/ingestion/parsing-processor.d.ts +9 -0
- package/dist/core/ingestion/parsing-processor.js +10 -0
- package/dist/core/ingestion/registry-primary-flag.d.ts +59 -0
- package/dist/core/ingestion/registry-primary-flag.js +78 -0
- package/dist/core/ingestion/scope-extractor-bridge.d.ts +32 -0
- package/dist/core/ingestion/scope-extractor-bridge.js +44 -0
- package/dist/core/ingestion/scope-extractor.d.ts +87 -0
- package/dist/core/ingestion/scope-extractor.js +603 -0
- package/dist/core/ingestion/shadow-harness.d.ts +113 -0
- package/dist/core/ingestion/shadow-harness.js +148 -0
- package/dist/core/ingestion/workers/parse-worker.d.ts +9 -0
- package/dist/core/ingestion/workers/parse-worker.js +20 -1
- package/dist/core/run-analyze.d.ts +21 -0
- package/dist/core/run-analyze.js +15 -4
- package/dist/core/search/phase-timer.d.ts +72 -0
- package/dist/core/search/phase-timer.js +106 -0
- package/dist/mcp/local/local-backend.js +70 -8
- package/dist/storage/git.d.ts +25 -0
- package/dist/storage/git.js +52 -0
- package/dist/storage/repo-manager.d.ts +70 -1
- package/dist/storage/repo-manager.js +107 -5
- package/package.json +1 -1
|
@@ -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
|
+
}
|
|
@@ -4,6 +4,7 @@ import { type MixedChainStep } from '../utils/call-analysis.js';
|
|
|
4
4
|
import type { ConstructorBinding } from '../type-env.js';
|
|
5
5
|
import type { NamedBinding } from '../named-bindings/types.js';
|
|
6
6
|
import type { NodeLabel } from '../../../_shared/index.js';
|
|
7
|
+
import type { ParsedFile } from '../../../_shared/index.js';
|
|
7
8
|
interface ParsedNode {
|
|
8
9
|
id: string;
|
|
9
10
|
label: string;
|
|
@@ -174,6 +175,14 @@ export interface ParseWorkerResult {
|
|
|
174
175
|
constructorBindings: FileConstructorBindings[];
|
|
175
176
|
/** All-scope type bindings from TypeEnv for BindingAccumulator (includes function-local). */
|
|
176
177
|
fileScopeBindings: FileScopeBindings[];
|
|
178
|
+
/**
|
|
179
|
+
* Per-file `ParsedFile` artifacts from the new scope-based resolution
|
|
180
|
+
* pipeline (RFC #909 Ring 2). Empty unless the file's provider implements
|
|
181
|
+
* `emitScopeCaptures` — default for every language today, so this is
|
|
182
|
+
* additive and leaves the legacy DAG untouched. Consumed by #921's
|
|
183
|
+
* finalize-orchestrator.
|
|
184
|
+
*/
|
|
185
|
+
parsedFiles: ParsedFile[];
|
|
177
186
|
skippedLanguages: Record<string, number>;
|
|
178
187
|
fileCount: number;
|
|
179
188
|
}
|
|
@@ -43,6 +43,7 @@ import { generateId } from '../../../lib/utils.js';
|
|
|
43
43
|
import { preprocessImportPath } from '../import-processor.js';
|
|
44
44
|
import { extractVueScript, extractTemplateComponents, isVueSetupTopLevel, } from '../vue-sfc-extractor.js';
|
|
45
45
|
import { buildMethodProps, arityForIdFromInfo, typeTagForId, constTagForId, buildCollisionGroups, } from '../utils/method-props.js';
|
|
46
|
+
import { extractParsedFile } from '../scope-extractor-bridge.js';
|
|
46
47
|
// ============================================================================
|
|
47
48
|
// Worker-local parser + language map
|
|
48
49
|
// ============================================================================
|
|
@@ -415,6 +416,7 @@ const processBatch = (files, onProgress) => {
|
|
|
415
416
|
ormQueries: [],
|
|
416
417
|
constructorBindings: [],
|
|
417
418
|
fileScopeBindings: [],
|
|
419
|
+
parsedFiles: [],
|
|
418
420
|
skippedLanguages: {},
|
|
419
421
|
fileCount: 0,
|
|
420
422
|
};
|
|
@@ -1017,11 +1019,25 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
|
|
|
1017
1019
|
console.warn(`Query execution failed for ${file.path}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1018
1020
|
continue;
|
|
1019
1021
|
}
|
|
1022
|
+
const provider = getProvider(language);
|
|
1023
|
+
// RFC #909 Ring 2: produce a `ParsedFile` for the new scope-based
|
|
1024
|
+
// resolution pipeline. No-op (returns undefined) for every language
|
|
1025
|
+
// today — only fires once a provider implements `emitScopeCaptures`.
|
|
1026
|
+
// Runs BEFORE legacy extraction and its result is independent: a
|
|
1027
|
+
// failure here is caught inside `extractParsedFile` and does NOT
|
|
1028
|
+
// affect the legacy DAG path that follows.
|
|
1029
|
+
const parsedFile = extractParsedFile(provider, parseContent, file.path, (message) => {
|
|
1030
|
+
if (parentPort)
|
|
1031
|
+
parentPort.postMessage({ type: 'warning', message });
|
|
1032
|
+
else
|
|
1033
|
+
console.warn(message);
|
|
1034
|
+
});
|
|
1035
|
+
if (parsedFile !== undefined)
|
|
1036
|
+
result.parsedFiles.push(parsedFile);
|
|
1020
1037
|
// Pre-pass: extract heritage from query matches to build parentMap for buildTypeEnv.
|
|
1021
1038
|
// Heritage edges (EXTENDS/IMPLEMENTS) are created by heritage-processor which runs
|
|
1022
1039
|
// in PARALLEL with call-processor, so the graph edges don't exist when buildTypeEnv
|
|
1023
1040
|
// runs. This pre-pass makes parent class information available for type resolution.
|
|
1024
|
-
const provider = getProvider(language);
|
|
1025
1041
|
const fileParentMap = new Map();
|
|
1026
1042
|
if (provider.heritageExtractor) {
|
|
1027
1043
|
for (const match of matches) {
|
|
@@ -1804,6 +1820,7 @@ let accumulated = {
|
|
|
1804
1820
|
ormQueries: [],
|
|
1805
1821
|
constructorBindings: [],
|
|
1806
1822
|
fileScopeBindings: [],
|
|
1823
|
+
parsedFiles: [],
|
|
1807
1824
|
skippedLanguages: {},
|
|
1808
1825
|
fileCount: 0,
|
|
1809
1826
|
};
|
|
@@ -1830,6 +1847,7 @@ const mergeResult = (target, src) => {
|
|
|
1830
1847
|
appendAll(target.ormQueries, src.ormQueries);
|
|
1831
1848
|
appendAll(target.constructorBindings, src.constructorBindings);
|
|
1832
1849
|
appendAll(target.fileScopeBindings, src.fileScopeBindings);
|
|
1850
|
+
appendAll(target.parsedFiles, src.parsedFiles);
|
|
1833
1851
|
for (const [lang, count] of Object.entries(src.skippedLanguages)) {
|
|
1834
1852
|
target.skippedLanguages[lang] = (target.skippedLanguages[lang] || 0) + count;
|
|
1835
1853
|
}
|
|
@@ -1878,6 +1896,7 @@ parentPort.on('message', (msg) => {
|
|
|
1878
1896
|
ormQueries: [],
|
|
1879
1897
|
constructorBindings: [],
|
|
1880
1898
|
fileScopeBindings: [],
|
|
1899
|
+
parsedFiles: [],
|
|
1881
1900
|
skippedLanguages: {},
|
|
1882
1901
|
fileCount: 0,
|
|
1883
1902
|
};
|
|
@@ -13,6 +13,12 @@ export interface AnalyzeCallbacks {
|
|
|
13
13
|
onLog?: (message: string) => void;
|
|
14
14
|
}
|
|
15
15
|
export interface AnalyzeOptions {
|
|
16
|
+
/**
|
|
17
|
+
* Force a full re-index of the pipeline. Callers may OR this with
|
|
18
|
+
* other flags that imply re-analysis (e.g. `--skills`), so the value
|
|
19
|
+
* here is the PIPELINE-force signal, NOT the registry-collision
|
|
20
|
+
* bypass. See `allowDuplicateName` below.
|
|
21
|
+
*/
|
|
16
22
|
force?: boolean;
|
|
17
23
|
embeddings?: boolean;
|
|
18
24
|
skipGit?: boolean;
|
|
@@ -20,6 +26,21 @@ export interface AnalyzeOptions {
|
|
|
20
26
|
skipAgentsMd?: boolean;
|
|
21
27
|
/** Omit volatile symbol/relationship counts from AGENTS.md and CLAUDE.md. */
|
|
22
28
|
noStats?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* User-provided alias for the registry `name` (#829). When set,
|
|
31
|
+
* forwarded to `registerRepo` so the indexed repo is stored under
|
|
32
|
+
* this alias instead of the path-derived basename.
|
|
33
|
+
*/
|
|
34
|
+
registryName?: string;
|
|
35
|
+
/**
|
|
36
|
+
* Bypass the `RegistryNameCollisionError` guard and allow two paths
|
|
37
|
+
* to register under the same `name` (#829). Controlled by the
|
|
38
|
+
* dedicated `--allow-duplicate-name` CLI flag, intentionally
|
|
39
|
+
* independent from `--force` — users who hit the collision guard
|
|
40
|
+
* should be able to accept the duplicate without paying the cost
|
|
41
|
+
* of a pipeline re-index.
|
|
42
|
+
*/
|
|
43
|
+
allowDuplicateName?: boolean;
|
|
23
44
|
}
|
|
24
45
|
export interface AnalyzeResult {
|
|
25
46
|
repoName: string;
|
package/dist/core/run-analyze.js
CHANGED
|
@@ -13,7 +13,7 @@ import fs from 'fs/promises';
|
|
|
13
13
|
import { runPipelineFromRepo } from './ingestion/pipeline.js';
|
|
14
14
|
import { initLbug, loadGraphToLbug, getLbugStats, executeQuery, executeWithReusedStatement, closeLbug, createFTSIndex, loadCachedEmbeddings, } from './lbug/lbug-adapter.js';
|
|
15
15
|
import { getStoragePaths, saveMeta, loadMeta, addToGitignore, registerRepo, cleanupOldKuzuFiles, } from '../storage/repo-manager.js';
|
|
16
|
-
import { getCurrentCommit, hasGitDir } from '../storage/git.js';
|
|
16
|
+
import { getCurrentCommit, hasGitDir, getInferredRepoName } from '../storage/git.js';
|
|
17
17
|
import { generateAIContextFiles } from '../cli/ai-context.js';
|
|
18
18
|
import { EMBEDDING_TABLE_NAME } from './lbug/schema.js';
|
|
19
19
|
import { STALE_HASH_SENTINEL } from './lbug/schema.js';
|
|
@@ -65,7 +65,7 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
|
|
|
65
65
|
// Non-git folders have currentCommit = '' — always rebuild since we can't detect changes
|
|
66
66
|
if (currentCommit !== '') {
|
|
67
67
|
return {
|
|
68
|
-
repoName: path.basename(repoPath),
|
|
68
|
+
repoName: options.registryName ?? getInferredRepoName(repoPath) ?? path.basename(repoPath),
|
|
69
69
|
repoPath,
|
|
70
70
|
stats: existingMeta.stats ?? {},
|
|
71
71
|
alreadyUpToDate: true,
|
|
@@ -218,12 +218,23 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
|
|
|
218
218
|
},
|
|
219
219
|
};
|
|
220
220
|
await saveMeta(storagePath, meta);
|
|
221
|
-
|
|
221
|
+
// Forward the --name alias and the registry-collision bypass bit.
|
|
222
|
+
// `allowDuplicateName` is its own concern — independent from the
|
|
223
|
+
// pipeline `force` above. The CLI maps it from
|
|
224
|
+
// `--allow-duplicate-name` only; `--force` and `--skills` both
|
|
225
|
+
// trigger pipeline re-run but never bypass the registry guard.
|
|
226
|
+
// The returned name is the one actually written to the registry
|
|
227
|
+
// (after applying the precedence chain in registerRepo) — reuse it
|
|
228
|
+
// so AGENTS.md / skill files reference the same name MCP clients
|
|
229
|
+
// will look up (#979).
|
|
230
|
+
const projectName = await registerRepo(repoPath, meta, {
|
|
231
|
+
name: options.registryName,
|
|
232
|
+
allowDuplicateName: options.allowDuplicateName,
|
|
233
|
+
});
|
|
222
234
|
// Only attempt to update .gitignore when a .git directory is present.
|
|
223
235
|
if (hasGitDir(repoPath)) {
|
|
224
236
|
await addToGitignore(repoPath);
|
|
225
237
|
}
|
|
226
|
-
const projectName = path.basename(repoPath);
|
|
227
238
|
// ── Generate AI context files (best-effort) ───────────────────────
|
|
228
239
|
let aggregatedClusterCount = 0;
|
|
229
240
|
if (pipelineResult.communityResult?.communities) {
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-phase wall-clock timing for the search pipeline and similar
|
|
3
|
+
* multi-stage flows. Designed to be called from query() with minimal
|
|
4
|
+
* ceremony and negligible overhead (< 0.1 ms per phase recorded).
|
|
5
|
+
*
|
|
6
|
+
* ### Sequential usage
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* const t = new PhaseTimer();
|
|
10
|
+
* t.start('bm25'); await bm25Search(...); t.stop();
|
|
11
|
+
* t.start('merge'); doMerge(); t.stop();
|
|
12
|
+
* const phases = t.summary(); // { bm25: 42, merge: 3 }
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* ### Concurrent usage (Promise.all)
|
|
16
|
+
*
|
|
17
|
+
* `start`/`stop` assume a single active phase at a time, which is wrong
|
|
18
|
+
* for concurrent work inside `Promise.all` — the second `start` would
|
|
19
|
+
* auto-stop the first and only one of the two would get timed. Use
|
|
20
|
+
* {@link PhaseTimer.time} to wrap each concurrent promise instead:
|
|
21
|
+
*
|
|
22
|
+
* ```ts
|
|
23
|
+
* const [a, b] = await Promise.all([
|
|
24
|
+
* t.time('bm25', bm25Search(...)),
|
|
25
|
+
* t.time('vector', semanticSearch(...)),
|
|
26
|
+
* ]);
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* ### Pre-measured durations
|
|
30
|
+
*
|
|
31
|
+
* ```ts
|
|
32
|
+
* t.mark('inherited', 12.5);
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export declare class PhaseTimer {
|
|
36
|
+
private phases;
|
|
37
|
+
private current;
|
|
38
|
+
private t0;
|
|
39
|
+
/** Start a new phase. Implicitly stops the previous one, if any. */
|
|
40
|
+
start(phase: string): void;
|
|
41
|
+
/** Stop the current phase. No-op if no phase is active. */
|
|
42
|
+
stop(): void;
|
|
43
|
+
/**
|
|
44
|
+
* Record a pre-measured duration without touching the active phase.
|
|
45
|
+
* Use for concurrent operations inside `Promise.all` where
|
|
46
|
+
* `start`/`stop` would step on each other, or for durations imported
|
|
47
|
+
* from sub-systems. Additive across repeated calls with the same
|
|
48
|
+
* phase name. Ignores negative / non-finite inputs.
|
|
49
|
+
*/
|
|
50
|
+
mark(phase: string, durationMs: number): void;
|
|
51
|
+
/**
|
|
52
|
+
* Wrap a promise with automatic timing. Records wall time via
|
|
53
|
+
* {@link PhaseTimer.mark} regardless of which other phases are
|
|
54
|
+
* active — safe to use inside `Promise.all`.
|
|
55
|
+
*/
|
|
56
|
+
time<T>(phase: string, promise: Promise<T>): Promise<T>;
|
|
57
|
+
/**
|
|
58
|
+
* Snapshot of accumulated durations rounded to 0.1 ms. Stops the
|
|
59
|
+
* current phase if one is still running.
|
|
60
|
+
*/
|
|
61
|
+
summary(): Record<string, number>;
|
|
62
|
+
/**
|
|
63
|
+
* Sum of every recorded phase duration.
|
|
64
|
+
*
|
|
65
|
+
* Note: for phases recorded via {@link PhaseTimer.time} or
|
|
66
|
+
* {@link PhaseTimer.mark} this is the *sum*, not the wall time —
|
|
67
|
+
* concurrent work overlaps and the sum can exceed the end-to-end
|
|
68
|
+
* wall time. Record wall time separately with `mark('wall', …)` if
|
|
69
|
+
* that distinction matters.
|
|
70
|
+
*/
|
|
71
|
+
totalMs(): number;
|
|
72
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-phase wall-clock timing for the search pipeline and similar
|
|
3
|
+
* multi-stage flows. Designed to be called from query() with minimal
|
|
4
|
+
* ceremony and negligible overhead (< 0.1 ms per phase recorded).
|
|
5
|
+
*
|
|
6
|
+
* ### Sequential usage
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* const t = new PhaseTimer();
|
|
10
|
+
* t.start('bm25'); await bm25Search(...); t.stop();
|
|
11
|
+
* t.start('merge'); doMerge(); t.stop();
|
|
12
|
+
* const phases = t.summary(); // { bm25: 42, merge: 3 }
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* ### Concurrent usage (Promise.all)
|
|
16
|
+
*
|
|
17
|
+
* `start`/`stop` assume a single active phase at a time, which is wrong
|
|
18
|
+
* for concurrent work inside `Promise.all` — the second `start` would
|
|
19
|
+
* auto-stop the first and only one of the two would get timed. Use
|
|
20
|
+
* {@link PhaseTimer.time} to wrap each concurrent promise instead:
|
|
21
|
+
*
|
|
22
|
+
* ```ts
|
|
23
|
+
* const [a, b] = await Promise.all([
|
|
24
|
+
* t.time('bm25', bm25Search(...)),
|
|
25
|
+
* t.time('vector', semanticSearch(...)),
|
|
26
|
+
* ]);
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* ### Pre-measured durations
|
|
30
|
+
*
|
|
31
|
+
* ```ts
|
|
32
|
+
* t.mark('inherited', 12.5);
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export class PhaseTimer {
|
|
36
|
+
phases = new Map();
|
|
37
|
+
current = null;
|
|
38
|
+
t0 = 0;
|
|
39
|
+
/** Start a new phase. Implicitly stops the previous one, if any. */
|
|
40
|
+
start(phase) {
|
|
41
|
+
this.stop();
|
|
42
|
+
this.current = phase;
|
|
43
|
+
this.t0 = performance.now();
|
|
44
|
+
}
|
|
45
|
+
/** Stop the current phase. No-op if no phase is active. */
|
|
46
|
+
stop() {
|
|
47
|
+
if (this.current !== null) {
|
|
48
|
+
const elapsed = performance.now() - this.t0;
|
|
49
|
+
this.phases.set(this.current, (this.phases.get(this.current) ?? 0) + elapsed);
|
|
50
|
+
this.current = null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Record a pre-measured duration without touching the active phase.
|
|
55
|
+
* Use for concurrent operations inside `Promise.all` where
|
|
56
|
+
* `start`/`stop` would step on each other, or for durations imported
|
|
57
|
+
* from sub-systems. Additive across repeated calls with the same
|
|
58
|
+
* phase name. Ignores negative / non-finite inputs.
|
|
59
|
+
*/
|
|
60
|
+
mark(phase, durationMs) {
|
|
61
|
+
if (!Number.isFinite(durationMs) || durationMs < 0)
|
|
62
|
+
return;
|
|
63
|
+
this.phases.set(phase, (this.phases.get(phase) ?? 0) + durationMs);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Wrap a promise with automatic timing. Records wall time via
|
|
67
|
+
* {@link PhaseTimer.mark} regardless of which other phases are
|
|
68
|
+
* active — safe to use inside `Promise.all`.
|
|
69
|
+
*/
|
|
70
|
+
async time(phase, promise) {
|
|
71
|
+
const t0 = performance.now();
|
|
72
|
+
try {
|
|
73
|
+
return await promise;
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
this.mark(phase, performance.now() - t0);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Snapshot of accumulated durations rounded to 0.1 ms. Stops the
|
|
81
|
+
* current phase if one is still running.
|
|
82
|
+
*/
|
|
83
|
+
summary() {
|
|
84
|
+
this.stop();
|
|
85
|
+
const out = {};
|
|
86
|
+
for (const [k, v] of this.phases)
|
|
87
|
+
out[k] = Math.round(v * 10) / 10;
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Sum of every recorded phase duration.
|
|
92
|
+
*
|
|
93
|
+
* Note: for phases recorded via {@link PhaseTimer.time} or
|
|
94
|
+
* {@link PhaseTimer.mark} this is the *sum*, not the wall time —
|
|
95
|
+
* concurrent work overlaps and the sum can exceed the end-to-end
|
|
96
|
+
* wall time. Record wall time separately with `mark('wall', …)` if
|
|
97
|
+
* that distinction matters.
|
|
98
|
+
*/
|
|
99
|
+
totalMs() {
|
|
100
|
+
this.stop();
|
|
101
|
+
let t = 0;
|
|
102
|
+
for (const v of this.phases.values())
|
|
103
|
+
t += v;
|
|
104
|
+
return Math.round(t * 10) / 10;
|
|
105
|
+
}
|
|
106
|
+
}
|