@event4u/agent-config 14.21.0 → 14.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.augment-plugin/marketplace.json +3 -3
- package/.augment-plugin/plugin.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +198 -0
- package/CREDITS.md +37 -0
- package/dist/agent-src/commands/analyze/inbox.md +380 -5
- package/dist/agent-src/commands/analyze/repo.md +4 -2
- package/dist/agent-src/contexts/communication/rules-auto/token-efficiency-mechanics.md +69 -0
- package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +5 -5
- package/dist/agent-src/rules/external-code-graph-interop.md +19 -14
- package/dist/agent-src/rules/token-efficiency.md +26 -3
- package/dist/agent-src/scripts/_cli_entry.ts +51 -0
- package/dist/agent-src/scripts/archive_completed_roadmaps.ts +14 -23
- package/dist/agent-src/scripts/roadmap_gates.ts +4 -20
- package/dist/agent-src/scripts/roadmap_set_step.ts +5 -2
- package/dist/agent-src/scripts/stubs_due.ts +4 -18
- package/dist/agent-src/scripts/update_roadmap_progress.ts +7 -24
- package/dist/agent-src/skills/code-intelligence/SKILL.md +25 -0
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +17 -0
- package/dist/agent-src/templates/agent-settings.md +0 -1
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli-delegate/_cli_entry.js +8 -0
- package/dist/cli-delegate/_lib/pin_resolver.js +170 -0
- package/dist/cli-delegate/archival_sweep.js +8 -0
- package/dist/cli-delegate/archive_completed_roadmaps.js +494 -0
- package/dist/cli-delegate/check_update_banner.js +923 -0
- package/dist/cli-delegate/chunk-5EK5WQN6.js +28 -0
- package/dist/cli-delegate/chunk-7XGD2OEU.js +96 -0
- package/dist/cli-delegate/{chunk-643Z7FIC.js → chunk-BIXEDQPZ.js} +6 -0
- package/dist/cli-delegate/chunk-DBH4AHDK.js +155 -0
- package/dist/cli-delegate/chunk-JO25G5H7.js +20 -0
- package/dist/cli-delegate/chunk-LTYVIY76.js +7961 -0
- package/dist/cli-delegate/chunk-QAJS5YWY.js +999 -0
- package/dist/cli-delegate/{chunk-ZVBOHSVF.js → chunk-TGIPZBCW.js} +1 -1
- package/dist/cli-delegate/{chunk-YS6XTLEK.js → chunk-TYSY33MD.js} +56 -6
- package/dist/cli-delegate/chunk-ULET4BYS.js +328 -0
- package/dist/cli-delegate/chunk-YDQSHTD7.js +187 -0
- package/dist/cli-delegate/chunk-YWVXLR44.js +48 -0
- package/dist/cli-delegate/chunk-ZZQBUHFK.js +273 -0
- package/dist/cli-delegate/cmd_conformance.js +2 -2
- package/dist/cli-delegate/cmd_export.js +1 -1
- package/dist/cli-delegate/cmd_handoff.js +26 -2
- package/dist/cli-delegate/cmd_prune.js +1 -1
- package/dist/cli-delegate/cmd_session_recycle.js +2 -2
- package/dist/cli-delegate/cmd_settings_migrate.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +1 -1
- package/dist/cli-delegate/cmd_uninstall.js +1 -1
- package/dist/cli-delegate/cmd_validate.js +1 -1
- package/dist/cli-delegate/dashboard_mode.js +16 -0
- package/dist/cli-delegate/gate_budget.js +24 -0
- package/dist/cli-delegate/gate_execute.js +25 -0
- package/dist/cli-delegate/guarded_baseline.js +18 -0
- package/dist/cli-delegate/resume_probe.js +22 -0
- package/dist/cli-delegate/roadmap_gates.js +638 -0
- package/dist/cli-delegate/roadmap_set_step.js +173 -0
- package/dist/cli-delegate/stub_queue.js +47 -0
- package/dist/cli-delegate/stubs_due.js +38 -0
- package/dist/cli-delegate/ticket_bundles.js +59 -0
- package/dist/cli-delegate/update_roadmap_progress.js +60 -0
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +8 -8
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +2 -2
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +2 -2
- package/dist/hooks/dispatch.js +1193 -917
- package/dist/install/claudeLayerCarriage.js +237 -0
- package/dist/install/claudePathsPlan.js +79 -0
- package/dist/install/hostLayerFingerprint.js +1 -1
- package/dist/install/install.mjs +6 -0
- package/dist/install/partitionEligibility.js +231 -117
- package/dist/install/ruleLayerPartition.js +10 -7
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +39219 -38995
- package/dist/scripts/condense.js +32 -25
- package/docs/MIGRATION.md +49 -1
- package/docs/contracts/settings-classes.md +1 -1
- package/docs/guidelines/agent-infra/minimal-safe-diff-mechanics.md +73 -0
- package/package.json +7 -4
- package/src/agent-src/scripts/_cli_entry.ts +51 -0
- package/src/agent-src/scripts/archive_completed_roadmaps.ts +14 -23
- package/src/agent-src/scripts/roadmap_gates.ts +4 -20
- package/src/agent-src/scripts/roadmap_set_step.ts +5 -2
- package/src/agent-src/scripts/stubs_due.ts +4 -18
- package/src/agent-src/scripts/update_roadmap_progress.ts +7 -24
- package/src/config/continuity-surface.json +509 -0
- package/src/config/estate-count-budget.json +1 -1
- package/src/config/gate-coverage.yml +29 -8
- package/src/config/gate-violation-baselines.json +8 -8
- package/src/config/gitignore-block.txt +19 -0
- package/src/config/host-injection-effect.json +73 -0
- package/src/config/packed-binary-manifest.json +33 -0
- package/src/config/preamble-payload-budget.json +112 -105
- package/src/config/publish-surface.json +4 -2
- package/src/install/claudeLayerCarriage.ts +273 -0
- package/src/install/claudePathsPlan.ts +76 -0
- package/src/install/hostLayerFingerprint.ts +1 -1
- package/src/install/partitionEligibility.ts +261 -127
- package/src/install/ruleLayerPartition.ts +19 -23
- package/src/scripts/_dispatch.bash +77 -27
- package/src/scripts/_lib/auto_dispatch.ts +2 -2
- package/src/scripts/_lib/catalogue_layer_parity.ts +146 -0
- package/src/scripts/_lib/continuity_surface.ts +296 -0
- package/src/scripts/_lib/corpus_manifest.ts +26 -9
- package/src/scripts/_lib/hook_settings.ts +1 -1
- package/src/scripts/_lib/injection_effect.ts +151 -0
- package/src/scripts/_lib/layer_overlap_notice.ts +50 -6
- package/src/scripts/_lib/packed_binary_predicate.ts +225 -0
- package/src/scripts/_lib/pin_resolver.ts +13 -0
- package/src/scripts/_lib/regression_neighbourhood.ts +1 -1
- package/src/scripts/_lib/retired_status.ts +74 -0
- package/src/scripts/_lib/skill_catalogue.ts +156 -21
- package/src/scripts/_lib/subagent_capsule.ts +117 -8
- package/src/scripts/ai_council/api_transport.ts +5 -0
- package/src/scripts/ai_council/deliberation_metrics.ts +353 -0
- package/src/scripts/ai_council/prompts.ts +9 -0
- package/src/scripts/ai_council/synthesis_disagreement.ts +206 -0
- package/src/scripts/capture_skill_catalogue.ts +24 -11
- package/src/scripts/check_candidate_lines.ts +499 -0
- package/src/scripts/check_continuity_surface.ts +475 -0
- package/src/scripts/check_dependency_floors.ts +13 -6
- package/src/scripts/check_estate_count.ts +40 -79
- package/src/scripts/check_pack_size.ts +262 -23
- package/src/scripts/check_roadmap_trackable.ts +19 -16
- package/src/scripts/check_rule_layer_partition.ts +54 -34
- package/src/scripts/check_single_delivery.ts +97 -31
- package/src/scripts/check_update_banner.ts +13 -0
- package/src/scripts/code_graph/build.ts +138 -15
- package/src/scripts/code_graph/cli.ts +76 -8
- package/src/scripts/code_graph/extract.ts +11 -0
- package/src/scripts/code_graph/grammar_probe.ts +43 -0
- package/src/scripts/code_graph/loader.ts +88 -14
- package/src/scripts/code_graph/query.ts +121 -11
- package/src/scripts/code_graph/resolution_tiers.ts +238 -0
- package/src/scripts/code_graph/sqlite_store.ts +247 -6
- package/src/scripts/code_graph/types.ts +66 -1
- package/src/scripts/code_graph/validate.ts +23 -0
- package/src/scripts/condense.ts +34 -22
- package/src/scripts/corpus_manifest.ts +5 -5
- package/src/scripts/council_cli.ts +11 -12
- package/src/scripts/count_intent_disagreement.ts +426 -0
- package/src/scripts/handoff_context_hook.ts +79 -0
- package/src/scripts/hook_effect_doctor.ts +82 -1
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +64 -12
- package/src/scripts/hooks/chain_nudge_hook.ts +282 -0
- package/src/scripts/hooks/code_graph_context_hook.ts +212 -0
- package/src/scripts/hooks/concern_registry.ts +4 -2
- package/src/scripts/hooks/host_semantics.ts +1 -1
- package/src/scripts/hooks/orchestration_record_hook.ts +2 -2
- package/src/scripts/hooks/rule_inject_hook.ts +1 -1
- package/src/scripts/hooks/skill_route_hook.ts +11 -4
- package/src/scripts/inbox_source_census.ts +322 -0
- package/src/scripts/install-hooks.sh +82 -0
- package/src/scripts/{lint_carrier_integrity.ts → lint_deferral_integrity.ts} +60 -214
- package/src/scripts/lint_plan_risk_register.ts +13 -17
- package/src/scripts/mcp_server/suggest_skill.ts +72 -0
- package/src/scripts/mcp_server/tools.ts +2 -56
- package/src/scripts/media/lib/adapter-contract.md +7 -2
- package/src/scripts/media/lib/fixtures/flux/asset-0001.png +0 -0
- package/src/scripts/media/lib/fixtures/gemini-image/asset-0001.png +0 -0
- package/src/scripts/media/lib/fixtures/ideogram/asset-0001.png +0 -0
- package/src/scripts/rdp_candidates_delta.ts +276 -0
- package/src/scripts/rdp_quality_eval.ts +217 -19
- package/src/scripts/release_mix_taxonomy.json +3 -2
- package/src/scripts/report_host_injection_effect.ts +189 -0
- package/src/scripts/report_layer_overlap.ts +41 -0
- package/src/scripts/report_skill_menu_census.ts +341 -0
- package/src/scripts/skill_tools/score_skill_relevance.ts +70 -6
- package/src/scripts/skill_tools/suggest_skill_for_task.ts +37 -3
- package/src/vendor/grammars/README.md +61 -0
- package/src/vendor/grammars/tree-sitter-javascript.wasm +0 -0
- package/src/vendor/grammars/tree-sitter-php.wasm +0 -0
- package/src/vendor/grammars/tree-sitter-typescript.wasm +0 -0
- package/src/scripts/_lib/carrier_status.ts +0 -118
- package/src/scripts/_lib/estate_carrier_cases.ts +0 -72
- package/src/scripts/hooks/code_graph_nudge_hook.ts +0 -191
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolution tiers that run BEFORE the repo-wide name lookup
|
|
3
|
+
* (road-to-a-graph-that-is-shipped 2.3).
|
|
4
|
+
*
|
|
5
|
+
* The extractor's ladder ends in a same-name table across the whole
|
|
6
|
+
* repository. That rung is a guess and is labelled one — `INFERRED`, via
|
|
7
|
+
* `name-lookup`. Two configuration files in a normal project state the answer
|
|
8
|
+
* outright, and reading them turns a guess into a fact:
|
|
9
|
+
*
|
|
10
|
+
* · `tsconfig.json` `compilerOptions.paths` — `@shared/x` IS `src/shared/x`,
|
|
11
|
+
* because the project says so. Without this tier the specifier is non-relative,
|
|
12
|
+
* so `resolveSpecifier` returns null and the import binds to `external:@shared/x`
|
|
13
|
+
* — a real module, correctly named, and the wrong one.
|
|
14
|
+
* · `composer.json` `autoload.psr-4` — `App\Services\Mailer` IS
|
|
15
|
+
* `app/Services/Mailer.php`. Without this tier a PHP `use` is matched by BASE
|
|
16
|
+
* NAME with the namespace discarded, so two classes called `Mailer` in
|
|
17
|
+
* different namespaces are indistinguishable.
|
|
18
|
+
*
|
|
19
|
+
* Both are DECLARED mappings, so an edge resolved through one is EXTRACTED, not
|
|
20
|
+
* INFERRED: the project's own config is as much a syntactic fact as an import
|
|
21
|
+
* specifier is.
|
|
22
|
+
*
|
|
23
|
+
* Deterministic and IO-free at resolution time — the configs are read once by
|
|
24
|
+
* the caller and passed in, so `buildGraph` stays pure and identical source
|
|
25
|
+
* plus identical config yields identical bytes.
|
|
26
|
+
*/
|
|
27
|
+
import type { ResolvedVia } from './types.js';
|
|
28
|
+
|
|
29
|
+
/** Extensions a TS/JS alias target may carry, in resolution order. */
|
|
30
|
+
const ALIAS_EXTS = ['.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs'] as const;
|
|
31
|
+
|
|
32
|
+
export interface AliasRule {
|
|
33
|
+
/** The pattern's literal prefix, e.g. `@shared/` for `@shared/*`. */
|
|
34
|
+
prefix: string;
|
|
35
|
+
/** Whether the pattern ended in `*` (a prefix rule) or was exact. */
|
|
36
|
+
wildcard: boolean;
|
|
37
|
+
/** Repo-relative substitution targets, already joined with `baseUrl`. */
|
|
38
|
+
targets: string[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface Psr4Rule {
|
|
42
|
+
/** Namespace prefix WITH its trailing separator, e.g. `App\\`. */
|
|
43
|
+
prefix: string;
|
|
44
|
+
/** Repo-relative directories, with a trailing slash. */
|
|
45
|
+
dirs: string[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface ResolutionConfig {
|
|
49
|
+
aliases: AliasRule[];
|
|
50
|
+
psr4: Psr4Rule[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const EMPTY_RESOLUTION_CONFIG: ResolutionConfig = { aliases: [], psr4: [] };
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Parse JSON that may carry comments and trailing commas.
|
|
57
|
+
*
|
|
58
|
+
* `tsconfig.json` is JSONC by convention and this repository's own uses `//`
|
|
59
|
+
* comments, so `JSON.parse` on it throws. Returning null rather than throwing
|
|
60
|
+
* is deliberate: an unparseable config must degrade to "no tiers", never break
|
|
61
|
+
* a build. Strings are walked rather than regex-stripped so a `//` inside a
|
|
62
|
+
* path value survives.
|
|
63
|
+
*/
|
|
64
|
+
export function parseJsonc(text: string): unknown | null {
|
|
65
|
+
let out = '';
|
|
66
|
+
let inString = false;
|
|
67
|
+
let escaped = false;
|
|
68
|
+
let i = 0;
|
|
69
|
+
while (i < text.length) {
|
|
70
|
+
const ch = text[i] as string;
|
|
71
|
+
if (inString) {
|
|
72
|
+
out += ch;
|
|
73
|
+
if (escaped) escaped = false;
|
|
74
|
+
else if (ch === '\\') escaped = true;
|
|
75
|
+
else if (ch === '"') inString = false;
|
|
76
|
+
i += 1;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (ch === '"') {
|
|
80
|
+
inString = true;
|
|
81
|
+
out += ch;
|
|
82
|
+
i += 1;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (ch === '/' && text[i + 1] === '/') {
|
|
86
|
+
while (i < text.length && text[i] !== '\n') i += 1;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (ch === '/' && text[i + 1] === '*') {
|
|
90
|
+
i += 2;
|
|
91
|
+
while (i < text.length && !(text[i] === '*' && text[i + 1] === '/')) i += 1;
|
|
92
|
+
i += 2;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
out += ch;
|
|
96
|
+
i += 1;
|
|
97
|
+
}
|
|
98
|
+
// Trailing commas before a closing brace/bracket.
|
|
99
|
+
out = out.replace(/,(\s*[}\]])/g, '$1');
|
|
100
|
+
try {
|
|
101
|
+
return JSON.parse(out) as unknown;
|
|
102
|
+
} catch {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function isRecord(v: unknown): v is Record<string, unknown> {
|
|
108
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Normalise a repo-relative path fragment: no leading `./`, no trailing `/`. */
|
|
112
|
+
function normalizeFragment(p: string): string {
|
|
113
|
+
return p.replace(/^\.\//, '').replace(/\/+$/, '');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Build the alias tier from a parsed `tsconfig.json`.
|
|
118
|
+
*
|
|
119
|
+
* `baseUrl` is resolved relative to the tsconfig's own directory, which for a
|
|
120
|
+
* root tsconfig is the repo root — so `baseUrl: "./src"` plus `"@shared/*":
|
|
121
|
+
* ["shared/*"]` yields the repo-relative prefix `src/shared/`.
|
|
122
|
+
*/
|
|
123
|
+
export function aliasRulesFrom(tsconfig: unknown): AliasRule[] {
|
|
124
|
+
if (!isRecord(tsconfig)) return [];
|
|
125
|
+
const co = tsconfig['compilerOptions'];
|
|
126
|
+
if (!isRecord(co)) return [];
|
|
127
|
+
const paths = co['paths'];
|
|
128
|
+
if (!isRecord(paths)) return [];
|
|
129
|
+
const baseUrl = typeof co['baseUrl'] === 'string' ? normalizeFragment(co['baseUrl']) : '';
|
|
130
|
+
const rules: AliasRule[] = [];
|
|
131
|
+
for (const [pattern, valueRaw] of Object.entries(paths)) {
|
|
132
|
+
if (!Array.isArray(valueRaw)) continue;
|
|
133
|
+
const wildcard = pattern.endsWith('*');
|
|
134
|
+
const prefix = wildcard ? pattern.slice(0, -1) : pattern;
|
|
135
|
+
const targets: string[] = [];
|
|
136
|
+
for (const v of valueRaw) {
|
|
137
|
+
if (typeof v !== 'string') continue;
|
|
138
|
+
const tgt = wildcard ? (v.endsWith('*') ? v.slice(0, -1) : v) : v;
|
|
139
|
+
const joined = baseUrl ? `${baseUrl}/${normalizeFragment(tgt)}` : normalizeFragment(tgt);
|
|
140
|
+
targets.push(joined.replace(/\/+$/, ''));
|
|
141
|
+
}
|
|
142
|
+
if (targets.length) rules.push({ prefix, wildcard, targets });
|
|
143
|
+
}
|
|
144
|
+
// Longest prefix first: `@shared/deep/` must beat `@shared/` when both are
|
|
145
|
+
// declared, which is TypeScript's own rule and not a preference.
|
|
146
|
+
rules.sort((a, b) => b.prefix.length - a.prefix.length);
|
|
147
|
+
return rules;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Build the PSR-4 tier from a parsed `composer.json`. */
|
|
151
|
+
export function psr4RulesFrom(composer: unknown): Psr4Rule[] {
|
|
152
|
+
if (!isRecord(composer)) return [];
|
|
153
|
+
const rules: Psr4Rule[] = [];
|
|
154
|
+
for (const key of ['autoload', 'autoload-dev']) {
|
|
155
|
+
const block = composer[key];
|
|
156
|
+
if (!isRecord(block)) continue;
|
|
157
|
+
const psr4 = block['psr-4'];
|
|
158
|
+
if (!isRecord(psr4)) continue;
|
|
159
|
+
for (const [ns, valueRaw] of Object.entries(psr4)) {
|
|
160
|
+
const dirsRaw = Array.isArray(valueRaw) ? valueRaw : [valueRaw];
|
|
161
|
+
const dirs = dirsRaw
|
|
162
|
+
.filter((d): d is string => typeof d === 'string')
|
|
163
|
+
.map((d) => {
|
|
164
|
+
const n = normalizeFragment(d);
|
|
165
|
+
return n === '' ? '' : `${n}/`;
|
|
166
|
+
});
|
|
167
|
+
if (dirs.length) rules.push({ prefix: ns, dirs });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// Longest namespace first — PSR-4's own most-specific-wins rule.
|
|
171
|
+
rules.sort((a, b) => b.prefix.length - a.prefix.length);
|
|
172
|
+
return rules;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function firstExisting(candidate: string, fileIds: ReadonlySet<string>): string | null {
|
|
176
|
+
if (fileIds.has(candidate)) return candidate;
|
|
177
|
+
// TS source imports `@shared/mailer.js` and the file on disk is
|
|
178
|
+
// `mailer.ts`, so the RUNTIME extension is stripped before the candidate
|
|
179
|
+
// list is tried — the same normalisation `resolveSpecifier` does for a
|
|
180
|
+
// relative specifier. Without it every alias in an ESM-style codebase
|
|
181
|
+
// misses, which is most of them.
|
|
182
|
+
const stem = candidate.replace(/\.(js|jsx|mjs|cjs)$/, '');
|
|
183
|
+
for (const e of ALIAS_EXTS) if (fileIds.has(stem + e)) return stem + e;
|
|
184
|
+
for (const e of ALIAS_EXTS) if (fileIds.has(`${candidate}/index${e}`)) return `${candidate}/index${e}`;
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Resolve a non-relative TS/JS specifier through the alias tier.
|
|
190
|
+
*
|
|
191
|
+
* Returns null when no rule matches or the mapped file is not in the graph —
|
|
192
|
+
* an alias pointing outside the built root is not a hit, and claiming it as one
|
|
193
|
+
* would be worse than the `external:` binding it replaces.
|
|
194
|
+
*/
|
|
195
|
+
export function resolveAlias(spec: string, config: ResolutionConfig, fileIds: ReadonlySet<string>): string | null {
|
|
196
|
+
for (const rule of config.aliases) {
|
|
197
|
+
if (rule.wildcard) {
|
|
198
|
+
if (!spec.startsWith(rule.prefix)) continue;
|
|
199
|
+
const rest = spec.slice(rule.prefix.length);
|
|
200
|
+
for (const t of rule.targets) {
|
|
201
|
+
const hit = firstExisting(rest ? `${t}/${rest}` : t, fileIds);
|
|
202
|
+
if (hit) return hit;
|
|
203
|
+
}
|
|
204
|
+
} else {
|
|
205
|
+
if (spec !== rule.prefix) continue;
|
|
206
|
+
for (const t of rule.targets) {
|
|
207
|
+
const hit = firstExisting(t, fileIds);
|
|
208
|
+
if (hit) return hit;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Resolve a fully-qualified PHP class name through the PSR-4 tier.
|
|
217
|
+
*
|
|
218
|
+
* `App\Services\Mailer` under `{"App\\": "app/"}` becomes
|
|
219
|
+
* `app/Services/Mailer.php`. A leading backslash is tolerated because `use
|
|
220
|
+
* \App\X` is legal and means the same thing.
|
|
221
|
+
*/
|
|
222
|
+
export function resolvePsr4(fqcn: string, config: ResolutionConfig, fileIds: ReadonlySet<string>): string | null {
|
|
223
|
+
const name = fqcn.replace(/^\\+/, '');
|
|
224
|
+
for (const rule of config.psr4) {
|
|
225
|
+
if (!name.startsWith(rule.prefix)) continue;
|
|
226
|
+
const rest = name.slice(rule.prefix.length).replace(/\\/g, '/');
|
|
227
|
+
if (!rest) continue;
|
|
228
|
+
for (const d of rule.dirs) {
|
|
229
|
+
const candidate = `${d}${rest}.php`;
|
|
230
|
+
if (fileIds.has(candidate)) return candidate;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** The `resolved_via` value each tier stamps on the edges it produces. */
|
|
237
|
+
export const ALIAS_VIA: ResolvedVia = 'path-alias';
|
|
238
|
+
export const PSR4_VIA: ResolvedVia = 'psr4';
|
|
@@ -32,8 +32,32 @@ import {
|
|
|
32
32
|
} from '../_lib/sqlite_guard.js';
|
|
33
33
|
import type { CodeEdge, CodeGraph, CodeNode } from './types.js';
|
|
34
34
|
|
|
35
|
-
/**
|
|
36
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Bump on ANY table/column change — a mismatch sends readers to the JSON.
|
|
37
|
+
*
|
|
38
|
+
* 1 → 2 (2.1): `edges` gained `resolved_via` and `provider`; `nodes` gained
|
|
39
|
+
* indexes on `label` and `source_file`; `meta` gained `node_count` /
|
|
40
|
+
* `edge_count`. A v1 twin has none of them, and the indexed read path below
|
|
41
|
+
* would silently answer from missing columns.
|
|
42
|
+
*
|
|
43
|
+
* 2 → 3 (2.2): those two columns are now REQUIRED on every edge. A v2 twin has
|
|
44
|
+
* the columns but may have written them NULL, from a graph built before the
|
|
45
|
+
* fields existed — which reads back as a valid edge carrying a fabricated
|
|
46
|
+
* mechanism. Refusing it costs nothing, because the twin is derived and the
|
|
47
|
+
* next build re-emits it.
|
|
48
|
+
*/
|
|
49
|
+
export const GRAPH_STORE_VERSION = 3;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Edge count at or above which the indexed read path replaces the blob path.
|
|
53
|
+
*
|
|
54
|
+
* A DECLARED threshold, not a measured crossover — nothing establishes that
|
|
55
|
+
* 50k is where the two paths actually cross. Below it, building the in-memory
|
|
56
|
+
* Maps is cheap enough that the extra SQL round-trips per hop are not worth
|
|
57
|
+
* paying; above it, `JSON.parse` plus two `Map`s over every edge is the
|
|
58
|
+
* dominant cost and it is paid before the first question is answered.
|
|
59
|
+
*/
|
|
60
|
+
export const INDEXED_READ_MIN_EDGES = 50_000;
|
|
37
61
|
|
|
38
62
|
/** Derived twin path for a JSON cache path.
|
|
39
63
|
* cache-invalidation: versioned via PRAGMA user_version (GRAPH_STORE_VERSION), not the filename. */
|
|
@@ -70,15 +94,25 @@ export function emitSqliteTwin(graph: CodeGraph, serialized: string, jsonPath: s
|
|
|
70
94
|
db.exec(
|
|
71
95
|
'CREATE TABLE meta (key TEXT PRIMARY KEY, value TEXT NOT NULL);' +
|
|
72
96
|
'CREATE TABLE nodes (id TEXT PRIMARY KEY, label TEXT NOT NULL, kind TEXT, source_file TEXT, l0 INTEGER, l1 INTEGER, l2 INTEGER, l3 INTEGER);' +
|
|
73
|
-
'CREATE TABLE edges (source TEXT NOT NULL, target TEXT NOT NULL, relation TEXT NOT NULL, confidence TEXT NOT NULL, candidates TEXT);' +
|
|
97
|
+
'CREATE TABLE edges (source TEXT NOT NULL, target TEXT NOT NULL, relation TEXT NOT NULL, confidence TEXT NOT NULL, candidates TEXT, ambiguity_reason TEXT, resolved_via TEXT, provider TEXT);' +
|
|
74
98
|
'CREATE INDEX edges_source ON edges(source);' +
|
|
75
|
-
'CREATE INDEX edges_target ON edges(target);'
|
|
99
|
+
'CREATE INDEX edges_target ON edges(target);' +
|
|
100
|
+
// Seed resolution and the changed-file seed set are the two
|
|
101
|
+
// reads that would otherwise scan every node. `id` needs no
|
|
102
|
+
// index — it is the PRIMARY KEY.
|
|
103
|
+
'CREATE INDEX nodes_label ON nodes(label);' +
|
|
104
|
+
'CREATE INDEX nodes_source_file ON nodes(source_file);',
|
|
76
105
|
);
|
|
77
106
|
const meta = db.prepare('INSERT INTO meta (key, value) VALUES (?, ?)');
|
|
78
107
|
meta.run('source_checksum', graph.source_checksum);
|
|
79
108
|
meta.run('json_size', String(stat.size));
|
|
80
109
|
meta.run('json_mtime_ms', String(stat.mtimeMs));
|
|
81
110
|
meta.run('graph_json', serialized);
|
|
111
|
+
// Counts live in meta so a reader can pick its path from one row
|
|
112
|
+
// instead of a COUNT(*) over the table it is deciding whether to
|
|
113
|
+
// read at all.
|
|
114
|
+
meta.run('node_count', String(graph.nodes.length));
|
|
115
|
+
meta.run('edge_count', String(graph.edges.length));
|
|
82
116
|
const insNode = db.prepare(
|
|
83
117
|
'INSERT INTO nodes (id, label, kind, source_file, l0, l1, l2, l3) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
|
|
84
118
|
);
|
|
@@ -96,10 +130,20 @@ export function emitSqliteTwin(graph: CodeGraph, serialized: string, jsonPath: s
|
|
|
96
130
|
);
|
|
97
131
|
}
|
|
98
132
|
const insEdge = db.prepare(
|
|
99
|
-
'INSERT INTO edges (source, target, relation, confidence, candidates
|
|
133
|
+
'INSERT INTO edges (source, target, relation, confidence, candidates, ambiguity_reason, resolved_via, provider) ' +
|
|
134
|
+
'VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
|
|
100
135
|
);
|
|
101
136
|
for (const e of graph.edges) {
|
|
102
|
-
insEdge.run(
|
|
137
|
+
insEdge.run(
|
|
138
|
+
e.source,
|
|
139
|
+
e.target,
|
|
140
|
+
e.relation,
|
|
141
|
+
e.confidence,
|
|
142
|
+
e.candidates ? JSON.stringify(e.candidates) : null,
|
|
143
|
+
e.ambiguity_reason ?? null,
|
|
144
|
+
e.resolved_via,
|
|
145
|
+
e.provider,
|
|
146
|
+
);
|
|
103
147
|
}
|
|
104
148
|
stampUserVersion(db, GRAPH_STORE_VERSION);
|
|
105
149
|
} finally {
|
|
@@ -150,6 +194,203 @@ export function loadSerializedFromTwin(jsonPath: string): string | null {
|
|
|
150
194
|
}
|
|
151
195
|
}
|
|
152
196
|
|
|
197
|
+
/**
|
|
198
|
+
* A per-node / per-edge read API over the twin — the thing this module did not
|
|
199
|
+
* export before (road-to-a-graph-that-is-shipped D7).
|
|
200
|
+
*
|
|
201
|
+
* Every export it had took a PATH and returned the whole graph: the serialized
|
|
202
|
+
* blob, or a pair of counts. So "the query path prefers SQLite" was true and
|
|
203
|
+
* bought nothing — `loadSerializedFromTwin` hands back the entire
|
|
204
|
+
* serialization, and the caller then parses it and builds a `Map` over every
|
|
205
|
+
* node and every edge. The twin was a cheaper blob transport, never an index.
|
|
206
|
+
*
|
|
207
|
+
* These reads take an id, a label, or a file, and touch only the rows that
|
|
208
|
+
* answer. Statements are prepared once per handle because a BFS calls
|
|
209
|
+
* `edgesTo` once per frontier node, and re-preparing per hop would hand back
|
|
210
|
+
* the constant factor the index just removed.
|
|
211
|
+
*
|
|
212
|
+
* The handle owns an open database and MUST be closed. Every method is safe
|
|
213
|
+
* after `close()` only in the sense that it will throw — callers hold it for
|
|
214
|
+
* the duration of one query and release it.
|
|
215
|
+
*/
|
|
216
|
+
export interface GraphIndex {
|
|
217
|
+
readonly nodeCount: number;
|
|
218
|
+
readonly edgeCount: number;
|
|
219
|
+
readonly sourceChecksum: string;
|
|
220
|
+
getNode(id: string): CodeNode | undefined;
|
|
221
|
+
hasNode(id: string): boolean;
|
|
222
|
+
/** Out-edges: everything `id` points at. */
|
|
223
|
+
edgesFrom(id: string): CodeEdge[];
|
|
224
|
+
/** In-edges: everything pointing at `id` — the reverse-BFS direction. */
|
|
225
|
+
edgesTo(id: string): CodeEdge[];
|
|
226
|
+
/** Exact-label resolution, served by `nodes_label`. */
|
|
227
|
+
idsByLabel(label: string, limit: number): string[];
|
|
228
|
+
/** Non-file nodes declared in any of `files` — the changed-seed lookup. */
|
|
229
|
+
idsInFiles(files: readonly string[]): string[];
|
|
230
|
+
/** Every node's id + searchable text. The ONLY whole-table read, and it
|
|
231
|
+
* exists because a BM25 fallback genuinely needs the corpus. Callers reach
|
|
232
|
+
* it only after an exact id AND an exact label both missed. */
|
|
233
|
+
lexicalCorpus(): { id: string; text: string }[];
|
|
234
|
+
close(): void;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
interface NodeRow {
|
|
238
|
+
id: string;
|
|
239
|
+
label: string;
|
|
240
|
+
kind: string | null;
|
|
241
|
+
source_file: string | null;
|
|
242
|
+
l0: number | null;
|
|
243
|
+
l1: number | null;
|
|
244
|
+
l2: number | null;
|
|
245
|
+
l3: number | null;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
interface EdgeRow {
|
|
249
|
+
source: string;
|
|
250
|
+
target: string;
|
|
251
|
+
relation: string;
|
|
252
|
+
confidence: string;
|
|
253
|
+
candidates: string | null;
|
|
254
|
+
ambiguity_reason: string | null;
|
|
255
|
+
resolved_via: string | null;
|
|
256
|
+
provider: string | null;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function toNode(r: NodeRow): CodeNode {
|
|
260
|
+
const loc = [r.l0, r.l1, r.l2, r.l3].filter((v): v is number => typeof v === 'number');
|
|
261
|
+
return {
|
|
262
|
+
id: r.id,
|
|
263
|
+
label: r.label,
|
|
264
|
+
kind: (r.kind ?? 'file') as CodeNode['kind'],
|
|
265
|
+
source_file: r.source_file ?? '',
|
|
266
|
+
source_location: loc,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function toEdge(r: EdgeRow): CodeEdge {
|
|
271
|
+
const e: CodeEdge = {
|
|
272
|
+
source: r.source,
|
|
273
|
+
target: r.target,
|
|
274
|
+
relation: r.relation as CodeEdge['relation'],
|
|
275
|
+
confidence: r.confidence as CodeEdge['confidence'],
|
|
276
|
+
// The SQL columns are nullable, the TypeScript fields are not. A null
|
|
277
|
+
// can only come from a twin emitted before these fields existed, which
|
|
278
|
+
// GRAPH_STORE_VERSION already refuses — so this coalesce is unreachable
|
|
279
|
+
// in a healthy tree and is written to fail READABLE rather than to
|
|
280
|
+
// fabricate: `name-lookup` is the weakest mechanism, so a mislabelled
|
|
281
|
+
// edge is under-trusted rather than over-trusted by Phase 3's filter.
|
|
282
|
+
resolved_via: (r.resolved_via ?? 'name-lookup') as CodeEdge['resolved_via'],
|
|
283
|
+
provider: (r.provider ?? 'native') as CodeEdge['provider'],
|
|
284
|
+
};
|
|
285
|
+
if (r.candidates) e.candidates = JSON.parse(r.candidates) as string[];
|
|
286
|
+
if (r.ambiguity_reason === 'receiver-unknown' || r.ambiguity_reason === 'hierarchy-unresolved') {
|
|
287
|
+
e.ambiguity_reason = r.ambiguity_reason;
|
|
288
|
+
}
|
|
289
|
+
return e;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Open the twin for INDEXED reads, or null when it cannot serve them.
|
|
294
|
+
*
|
|
295
|
+
* Null on exactly the conditions `loadSerializedFromTwin` already refuses on —
|
|
296
|
+
* absent, stale against the JSON's size/mtime, corrupt, version-mismatched, no
|
|
297
|
+
* `node:sqlite` — so the two paths agree about when the twin is trustworthy
|
|
298
|
+
* and a caller never has to ask twice.
|
|
299
|
+
*/
|
|
300
|
+
export function openGraphIndex(jsonPath: string): GraphIndex | null {
|
|
301
|
+
if (!isSqliteAvailableSync()) return null;
|
|
302
|
+
const twin = sqliteTwinPath(jsonPath);
|
|
303
|
+
if (!fs.existsSync(twin)) return null;
|
|
304
|
+
let stat: fs.Stats;
|
|
305
|
+
try {
|
|
306
|
+
stat = fs.statSync(jsonPath);
|
|
307
|
+
} catch {
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
try {
|
|
311
|
+
const { DatabaseSync } = loadSqliteSync('code_graph.sqlite_store');
|
|
312
|
+
const db = new DatabaseSync(twin, { readOnly: true });
|
|
313
|
+
try {
|
|
314
|
+
if (readUserVersion(db) !== GRAPH_STORE_VERSION) {
|
|
315
|
+
db.close();
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
const rows = db.prepare('SELECT key, value FROM meta').all() as { key: string; value: string }[];
|
|
319
|
+
const meta = new Map(rows.map((r) => [r.key, r.value]));
|
|
320
|
+
if (meta.get('json_size') !== String(stat.size) || meta.get('json_mtime_ms') !== String(stat.mtimeMs)) {
|
|
321
|
+
db.close();
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
const nodeCount = Number(meta.get('node_count') ?? NaN);
|
|
325
|
+
const edgeCount = Number(meta.get('edge_count') ?? NaN);
|
|
326
|
+
if (!Number.isFinite(nodeCount) || !Number.isFinite(edgeCount)) {
|
|
327
|
+
// A v2 twin always carries both. Missing means something wrote
|
|
328
|
+
// this file that is not this module, and guessing the counts
|
|
329
|
+
// would be worse than declining.
|
|
330
|
+
db.close();
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const qNode = db.prepare('SELECT * FROM nodes WHERE id = ?');
|
|
335
|
+
const qFrom = db.prepare('SELECT * FROM edges WHERE source = ?');
|
|
336
|
+
const qTo = db.prepare('SELECT * FROM edges WHERE target = ?');
|
|
337
|
+
const qLabel = db.prepare('SELECT id FROM nodes WHERE label = ? LIMIT ?');
|
|
338
|
+
|
|
339
|
+
return {
|
|
340
|
+
nodeCount,
|
|
341
|
+
edgeCount,
|
|
342
|
+
sourceChecksum: meta.get('source_checksum') ?? '',
|
|
343
|
+
getNode(id) {
|
|
344
|
+
const r = qNode.get(id) as unknown as NodeRow | undefined;
|
|
345
|
+
return r ? toNode(r) : undefined;
|
|
346
|
+
},
|
|
347
|
+
hasNode(id) {
|
|
348
|
+
return (qNode.get(id) as unknown as NodeRow | undefined) !== undefined;
|
|
349
|
+
},
|
|
350
|
+
edgesFrom(id) {
|
|
351
|
+
return (qFrom.all(id) as unknown as EdgeRow[]).map(toEdge);
|
|
352
|
+
},
|
|
353
|
+
edgesTo(id) {
|
|
354
|
+
return (qTo.all(id) as unknown as EdgeRow[]).map(toEdge);
|
|
355
|
+
},
|
|
356
|
+
idsByLabel(labelText, limit) {
|
|
357
|
+
return (qLabel.all(labelText, limit) as { id: string }[]).map((r) => r.id);
|
|
358
|
+
},
|
|
359
|
+
idsInFiles(files) {
|
|
360
|
+
if (files.length === 0) return [];
|
|
361
|
+
// Chunked because SQLite caps bound parameters (999 on the
|
|
362
|
+
// conservative default build) and a changed-file set from a
|
|
363
|
+
// wide diff can exceed it.
|
|
364
|
+
const out: string[] = [];
|
|
365
|
+
const CHUNK = 400;
|
|
366
|
+
for (let i = 0; i < files.length; i += CHUNK) {
|
|
367
|
+
const slice = files.slice(i, i + CHUNK);
|
|
368
|
+
const marks = slice.map(() => '?').join(',');
|
|
369
|
+
const stmt = db.prepare(
|
|
370
|
+
`SELECT id FROM nodes WHERE kind != 'file' AND source_file IN (${marks})`,
|
|
371
|
+
);
|
|
372
|
+
for (const r of stmt.all(...slice) as { id: string }[]) out.push(r.id);
|
|
373
|
+
}
|
|
374
|
+
return out;
|
|
375
|
+
},
|
|
376
|
+
lexicalCorpus() {
|
|
377
|
+
return (db.prepare('SELECT id, label FROM nodes').all() as { id: string; label: string }[]).map(
|
|
378
|
+
(r) => ({ id: r.id, text: `${r.label} ${r.id}` }),
|
|
379
|
+
);
|
|
380
|
+
},
|
|
381
|
+
close() {
|
|
382
|
+
db.close();
|
|
383
|
+
},
|
|
384
|
+
};
|
|
385
|
+
} catch (err) {
|
|
386
|
+
db.close();
|
|
387
|
+
throw err;
|
|
388
|
+
}
|
|
389
|
+
} catch {
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
153
394
|
/** Structured node/edge access for future in-process consumers (kept minimal:
|
|
154
395
|
* the loader above guarantees byte-identical answers; these are for tooling). */
|
|
155
396
|
export function twinCounts(jsonPath: string): { nodes: number; edges: number } | null {
|
|
@@ -17,6 +17,55 @@ export type EdgeConfidence = 'EXTRACTED' | 'INFERRED' | 'AMBIGUOUS';
|
|
|
17
17
|
/** Relations the extractor emits. */
|
|
18
18
|
export type Relation = 'calls' | 'imports' | 'uses' | 'inherits' | 'member';
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* HOW an edge's target was arrived at — the mechanism, not the confidence.
|
|
22
|
+
*
|
|
23
|
+
* The two axes are independent and conflating them is what this field fixes.
|
|
24
|
+
* `confidence` says how much to trust the target; `resolved_via` says what
|
|
25
|
+
* produced it, which is the axis a consumer filters on. Phase 3's verbs accept
|
|
26
|
+
* an edge when `resolved_via` is not in `{name-lookup, dynamic}` — a rule that
|
|
27
|
+
* cannot be expressed in the confidence scale at all, because a `name-lookup`
|
|
28
|
+
* hit and a same-file hit can both be INFERRED.
|
|
29
|
+
*
|
|
30
|
+
* Four of the eight occur today, and saying which is the point of an enum that
|
|
31
|
+
* is wider than its current population:
|
|
32
|
+
*
|
|
33
|
+
* · `same-file` — a local declaration in the using file. The language's own
|
|
34
|
+
* shadowing rule makes this the strongest binding there is.
|
|
35
|
+
* · `import-specifier` — an import/`use` binding: the file states where the
|
|
36
|
+
* name comes from.
|
|
37
|
+
* · `name-lookup` — the repo-wide same-name table, or an attempted lookup that
|
|
38
|
+
* found nothing and left a `symbol:` pseudo-target. A guess either way, and
|
|
39
|
+
* the value that says so.
|
|
40
|
+
* · `dynamic` — a receiver the extractor cannot type (`$obj->m()`, an
|
|
41
|
+
* unresolved `C::m()`), or a hierarchy rooted outside the repository.
|
|
42
|
+
*
|
|
43
|
+
* The remaining four are declared and currently unemitted: `path-alias` and
|
|
44
|
+
* `psr4` arrive with the resolution tiers in step 2.3, and `route-table` /
|
|
45
|
+
* `test-import` with the relations Phase 3 adds. They are in the union now so
|
|
46
|
+
* that adding them later is a build change rather than a schema change.
|
|
47
|
+
*/
|
|
48
|
+
export type ResolvedVia =
|
|
49
|
+
| 'same-file'
|
|
50
|
+
| 'import-specifier'
|
|
51
|
+
| 'path-alias'
|
|
52
|
+
| 'psr4'
|
|
53
|
+
| 'route-table'
|
|
54
|
+
| 'test-import'
|
|
55
|
+
| 'name-lookup'
|
|
56
|
+
| 'dynamic';
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* WHICH engine produced the edge.
|
|
60
|
+
*
|
|
61
|
+
* One value, and the field exists anyway: an edge with no provider is
|
|
62
|
+
* indistinguishable from an edge whose provider nobody recorded, and the whole
|
|
63
|
+
* point of a mixed graph — should one ever exist — is being able to tell them
|
|
64
|
+
* apart. Kill register K2 forbids an external provider entering `src/`, so this
|
|
65
|
+
* stays a single-member union until a decision changes that.
|
|
66
|
+
*/
|
|
67
|
+
export type EdgeProvider = 'native';
|
|
68
|
+
|
|
20
69
|
export interface CodeNode {
|
|
21
70
|
/** Path-qualified from day one: `<relpath>#<symbol>` (collision-free). */
|
|
22
71
|
id: string;
|
|
@@ -45,6 +94,14 @@ export interface CodeEdge {
|
|
|
45
94
|
target: string;
|
|
46
95
|
relation: Relation;
|
|
47
96
|
confidence: EdgeConfidence;
|
|
97
|
+
/**
|
|
98
|
+
* REQUIRED. Not optional, so "no edge lacks either field" is a property the
|
|
99
|
+
* type system enforces at every construction site rather than a rule a
|
|
100
|
+
* linter has to re-check afterwards.
|
|
101
|
+
*/
|
|
102
|
+
resolved_via: ResolvedVia;
|
|
103
|
+
/** REQUIRED, for the same reason. */
|
|
104
|
+
provider: EdgeProvider;
|
|
48
105
|
/**
|
|
49
106
|
* Present only on AMBIGUOUS / multi-candidate edges: the resolved-target
|
|
50
107
|
* candidates by id. An AMBIGUOUS edge is "correct" when the true target is
|
|
@@ -105,6 +162,14 @@ export interface CodeGraph {
|
|
|
105
162
|
}
|
|
106
163
|
|
|
107
164
|
/**
|
|
165
|
+
* Bumped 2 → 3 by `road-to-a-graph-that-is-shipped` 2.2: every `CodeEdge` now
|
|
166
|
+
* carries `resolved_via` and `provider`. A cached sidecar written at v2 has
|
|
167
|
+
* neither, so `--update` would reuse untagged edges beside tagged ones and
|
|
168
|
+
* produce a graph whose `resolved_via` histogram silently under-counts —
|
|
169
|
+
* exactly the mixed-graph failure the previous bump describes. `readSidecar`
|
|
170
|
+
* refuses a version mismatch, which is what makes that impossible rather than
|
|
171
|
+
* merely unlikely.
|
|
172
|
+
*
|
|
108
173
|
* Bumped 1 → 2 by `road-to-the-graph-that-lies-confidently`: `CodeNode.kind`
|
|
109
174
|
* gained three members, `CodeGraph` gained `suppressed_edge_counts`, and the
|
|
110
175
|
* per-file extract sidecar gained the import specifier. A cached sidecar
|
|
@@ -113,7 +178,7 @@ export interface CodeGraph {
|
|
|
113
178
|
* file — a silently mixed graph. `readSidecar` refuses a version mismatch, so
|
|
114
179
|
* the bump is what makes that impossible rather than merely unlikely.
|
|
115
180
|
*/
|
|
116
|
-
export const SCHEMA_VERSION =
|
|
181
|
+
export const SCHEMA_VERSION = 3;
|
|
117
182
|
|
|
118
183
|
/** Deterministic per-file byte cap — files above this become a SKIPPED node. */
|
|
119
184
|
export const MAX_FILE_BYTES = 1_000_000;
|
|
@@ -6,6 +6,19 @@ import type { CodeGraph } from './types.js';
|
|
|
6
6
|
|
|
7
7
|
const RELATIONS = new Set(['calls', 'imports', 'uses', 'inherits', 'member']);
|
|
8
8
|
const CONFIDENCES = new Set(['EXTRACTED', 'INFERRED', 'AMBIGUOUS']);
|
|
9
|
+
/** Mirrors `ResolvedVia` in types.ts — the full union, including the four
|
|
10
|
+
* values no build emits yet (2.3 and Phase 3 add them). */
|
|
11
|
+
const RESOLVED_VIA = new Set([
|
|
12
|
+
'same-file',
|
|
13
|
+
'import-specifier',
|
|
14
|
+
'path-alias',
|
|
15
|
+
'psr4',
|
|
16
|
+
'route-table',
|
|
17
|
+
'test-import',
|
|
18
|
+
'name-lookup',
|
|
19
|
+
'dynamic',
|
|
20
|
+
]);
|
|
21
|
+
const PROVIDERS = new Set(['native']);
|
|
9
22
|
const KINDS = new Set([
|
|
10
23
|
'file',
|
|
11
24
|
'class',
|
|
@@ -57,6 +70,16 @@ export function validateGraph(g: unknown): ValidateResult {
|
|
|
57
70
|
if (!RELATIONS.has(ee['relation'] as string)) push(`edge[${i}].relation invalid: ${String(ee['relation'])}`);
|
|
58
71
|
if (!CONFIDENCES.has(ee['confidence'] as string))
|
|
59
72
|
push(`edge[${i}].confidence invalid: ${String(ee['confidence'])}`);
|
|
73
|
+
// Both are REQUIRED (2.2). Checked as presence-and-membership rather
|
|
74
|
+
// than membership alone, because an absent field and a misspelled one
|
|
75
|
+
// are different defects: absent means the build did not tag the edge,
|
|
76
|
+
// misspelled means the taxonomy drifted.
|
|
77
|
+
if (ee['resolved_via'] === undefined) push(`edge[${i}].resolved_via is required`);
|
|
78
|
+
else if (!RESOLVED_VIA.has(ee['resolved_via'] as string))
|
|
79
|
+
push(`edge[${i}].resolved_via invalid: ${String(ee['resolved_via'])}`);
|
|
80
|
+
if (ee['provider'] === undefined) push(`edge[${i}].provider is required`);
|
|
81
|
+
else if (!PROVIDERS.has(ee['provider'] as string))
|
|
82
|
+
push(`edge[${i}].provider invalid: ${String(ee['provider'])}`);
|
|
60
83
|
// a source must be a known node; targets may be unresolved `symbol:` refs
|
|
61
84
|
if (typeof ee['source'] === 'string' && !ids.has(ee['source'] as string))
|
|
62
85
|
push(`edge[${i}].source is not a known node: ${String(ee['source'])}`);
|