@event4u/agent-config 14.21.0 → 14.23.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 +164 -187
- package/CREDITS.md +37 -0
- package/README.md +1 -1
- package/dist/agent-src/commands/analyze/inbox.md +380 -5
- package/dist/agent-src/commands/analyze/repo.md +4 -2
- package/dist/agent-src/commands/pr/merge.md +47 -38
- package/dist/agent-src/commands/roadmap/next.md +8 -0
- 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/contexts/execution/autonomy-mechanics.md +12 -0
- package/dist/agent-src/rules/external-code-graph-interop.md +19 -14
- package/dist/agent-src/rules/notes-first-reasoning.md +36 -25
- 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 +120 -27
- 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 +82 -1
- package/dist/agent-src/skills/git-workflow/references/push-closes-its-loop.md +9 -0
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +17 -0
- package/dist/agent-src/skills/tailwind-engineer/SKILL.md +52 -2
- 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/registry.js +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 +542 -0
- package/dist/cli-delegate/check_update_banner.js +923 -0
- package/dist/cli-delegate/{chunk-ZVBOHSVF.js → chunk-2KKXJW25.js} +23 -9
- package/dist/cli-delegate/chunk-5EK5WQN6.js +28 -0
- package/dist/cli-delegate/chunk-7XGD2OEU.js +96 -0
- package/dist/cli-delegate/chunk-DBH4AHDK.js +155 -0
- package/dist/cli-delegate/{chunk-4FS4WRXP.js → chunk-IAI3XL43.js} +15 -3
- package/dist/cli-delegate/chunk-JO25G5H7.js +20 -0
- package/dist/cli-delegate/{chunk-643Z7FIC.js → chunk-KCNTUSPI.js} +7 -1
- package/dist/cli-delegate/chunk-LTYVIY76.js +7961 -0
- package/dist/cli-delegate/chunk-QAJS5YWY.js +999 -0
- 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 +2 -2
- package/dist/cli-delegate/cmd_handoff.js +45 -13
- package/dist/cli-delegate/cmd_prune.js +2 -2
- package/dist/cli-delegate/cmd_session_recycle.js +4 -14
- package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
- package/dist/cli-delegate/cmd_settings_set.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +2 -2
- package/dist/cli-delegate/cmd_uninstall.js +2 -2
- package/dist/cli-delegate/cmd_validate.js +2 -2
- 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 +13 -13
- 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 +1813 -946
- 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 +21 -3
- 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 +10870 -7641
- package/dist/scripts/_lib/lean_projection_mode.js +108 -0
- package/dist/scripts/_lib/router_match.js +243 -0
- package/dist/scripts/_lib/rule_injection.js +216 -0
- package/dist/scripts/condense.js +52 -45
- package/dist/scripts/project_thin_rules.js +139 -19
- package/dist/server/schemas/settings.js +16 -3
- package/docs/MIGRATION.md +105 -1
- package/docs/contracts/settings-classes.md +40 -8
- package/docs/guidelines/agent-infra/minimal-safe-diff-mechanics.md +73 -0
- package/docs/guidelines/agent-infra/notes-horizon-mechanics.md +90 -0
- package/llms.txt +1 -1
- package/package.json +7 -4
- package/src/agent-src/scripts/_cli_entry.ts +51 -0
- package/src/agent-src/scripts/archive_completed_roadmaps.ts +120 -27
- 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/agent-settings.template.yml +47 -1
- package/src/config/continuity-surface.json +516 -0
- package/src/config/estate-count-budget.json +1 -1
- package/src/config/evaluator-budgets.json +4 -4
- 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/hook-token-budget.json +7 -6
- 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/config/routing-coverage-seed.json +9 -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/_cli/cmd_session_recycle.ts +2 -25
- package/src/scripts/_cli/handoff_sessions.ts +14 -44
- 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_slot.ts +295 -0
- package/src/scripts/_lib/continuity_surface.ts +296 -0
- package/src/scripts/_lib/continuity_writer.ts +253 -0
- package/src/scripts/_lib/corpus_manifest.ts +26 -9
- package/src/scripts/_lib/hook_settings.ts +86 -1
- package/src/scripts/_lib/injection_effect.ts +151 -0
- package/src/scripts/_lib/layer_overlap_notice.ts +50 -6
- package/src/scripts/_lib/lean_projection_mode.ts +127 -0
- package/src/scripts/_lib/packed_binary_predicate.ts +262 -0
- package/src/scripts/_lib/pin_resolver.ts +13 -0
- package/src/scripts/_lib/recycle_envelope_paths.ts +109 -21
- package/src/scripts/_lib/regression_neighbourhood.ts +159 -22
- package/src/scripts/_lib/retired_status.ts +74 -0
- package/src/scripts/_lib/session_eol.ts +44 -0
- package/src/scripts/_lib/session_index_trust.ts +346 -0
- package/src/scripts/_lib/skill_catalogue.ts +156 -21
- package/src/scripts/_lib/standing_bound_ratchet.ts +154 -0
- package/src/scripts/_lib/subagent_capsule.ts +117 -8
- package/src/scripts/_lib/vendored_grammar_upstream.ts +280 -0
- 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/audit_mcp_tools.ts +69 -25
- package/src/scripts/build_mcp_catalog.ts +24 -1
- 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_host_tree_parity.ts +291 -0
- package/src/scripts/check_pack_size.ts +262 -23
- package/src/scripts/check_preamble_payload_budget.ts +136 -7
- package/src/scripts/check_roadmap_trackable.ts +19 -16
- package/src/scripts/check_rule_layer_partition.ts +54 -34
- package/src/scripts/check_rule_projection_integrity.ts +102 -2
- package/src/scripts/check_single_delivery.ts +97 -31
- package/src/scripts/check_update_banner.ts +13 -0
- package/src/scripts/ci_settle.ts +205 -5
- package/src/scripts/code_graph/build.ts +217 -15
- package/src/scripts/code_graph/cli.ts +334 -15
- package/src/scripts/code_graph/detect.ts +38 -0
- 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 +144 -11
- package/src/scripts/code_graph/resolution_tiers.ts +238 -0
- package/src/scripts/code_graph/sqlite_store.ts +262 -6
- package/src/scripts/code_graph/types.ts +117 -3
- package/src/scripts/code_graph/validate.ts +26 -1
- package/src/scripts/code_graph/verbs.ts +590 -0
- package/src/scripts/condense.ts +54 -42
- 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 +118 -1
- package/src/scripts/hook_effect_doctor.ts +82 -1
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +87 -12
- package/src/scripts/hooks/block_speaking_inbox_dir.ts +182 -16
- package/src/scripts/hooks/chain_nudge_hook.ts +282 -0
- package/src/scripts/hooks/code_graph_context_hook.ts +201 -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/push_settle_hook.ts +3 -2
- package/src/scripts/hooks/rule_inject_hook.ts +108 -21
- package/src/scripts/hooks/session_eol_hook.ts +120 -2
- package/src/scripts/hooks/skill_route_hook.ts +11 -4
- package/src/scripts/hot_context_hook.ts +62 -7
- 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/lint_roadmap_blockers.ts +164 -1
- package/src/scripts/mcp_server/consumer_tool_catalog.json +152 -2
- package/src/scripts/mcp_server/graph_tools.ts +294 -0
- package/src/scripts/mcp_server/path_util.ts +31 -0
- package/src/scripts/mcp_server/suggest_skill.ts +72 -0
- package/src/scripts/mcp_server/tool_catalog_source.ts +7 -1
- package/src/scripts/mcp_server/tools.ts +5 -69
- 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/model_rule_injection.ts +61 -4
- package/src/scripts/project_thin_rules.ts +149 -16
- 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/report_standing_payload_by_host.ts +650 -0
- package/src/scripts/schemas/agent-settings.schema.json +7 -1
- package/src/scripts/session_memory_index.ts +81 -8
- 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/server/schemas/settings.ts +23 -4
- package/src/vendor/grammars/README.md +86 -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
|
@@ -28,6 +28,20 @@
|
|
|
28
28
|
* `agents/runtime/state/code-graph-v1.json`) is local-only and disposable —
|
|
29
29
|
* gitignored, rebuildable from source at any time; never a source of truth.
|
|
30
30
|
*
|
|
31
|
+
* impact --diff <rev> [--depth N] callers/dependents/test files
|
|
32
|
+
* reachable from the changed
|
|
33
|
+
* symbols over ACCEPTED edges
|
|
34
|
+
* tests-for <symbol> test files importing the symbol
|
|
35
|
+
* untested --diff <rev> changed symbols no test imports
|
|
36
|
+
* dead [--entry-points F] zero accepted in-edges, minus
|
|
37
|
+
* [--accept-missing-exports] declared entry points; REFUSES
|
|
38
|
+
* when a source is unavailable
|
|
39
|
+
*
|
|
40
|
+
* The four gate verbs (3.1–3.3) each print the `resolved_via` histogram of the
|
|
41
|
+
* edges they accepted AND of the edges they refused to walk, plus the graph's
|
|
42
|
+
* three-state staleness — so a caller can tell "no callers" from "no callers I
|
|
43
|
+
* would trust", and "dead" from "dead as of a graph 40 commits behind".
|
|
44
|
+
*
|
|
31
45
|
* Query subcommands (query/path/explain/affected) are added by the Phase-3
|
|
32
46
|
* query tier. Deterministic, LLM-free, no network. Exit codes:
|
|
33
47
|
* 0 ok · 1 not-found / validation-failed / refresh-budget-exceeded ·
|
|
@@ -49,8 +63,8 @@ import process from 'node:process';
|
|
|
49
63
|
import { fileURLToPath } from 'node:url';
|
|
50
64
|
|
|
51
65
|
import { hardenedSpawnEnv } from '../_lib/spawn_env.js';
|
|
52
|
-
import { buildFromRepo, sidecarPath } from './build.js';
|
|
53
|
-
import { computeVerdict, detectSources, pickSource } from './detect.js';
|
|
66
|
+
import { buildFromRepo, isTestFile, sidecarPath } from './build.js';
|
|
67
|
+
import { computeVerdict, detectSources, type GraphState, graphState, pickSource } from './detect.js';
|
|
54
68
|
import { suggestVerb } from './intent.js';
|
|
55
69
|
import {
|
|
56
70
|
affected,
|
|
@@ -62,7 +76,21 @@ import {
|
|
|
62
76
|
query,
|
|
63
77
|
type QueryResult,
|
|
64
78
|
} from './query.js';
|
|
79
|
+
import { openGraphIndex, sqliteTwinPath } from './sqlite_store.js';
|
|
80
|
+
import type { CodeGraph } from './types.js';
|
|
65
81
|
import { validateGraph } from './validate.js';
|
|
82
|
+
import {
|
|
83
|
+
dead,
|
|
84
|
+
type DeadResult,
|
|
85
|
+
entryPointsFromFile,
|
|
86
|
+
impact,
|
|
87
|
+
type ImpactResult,
|
|
88
|
+
repoEntryPoints,
|
|
89
|
+
testsFor,
|
|
90
|
+
type TestsForResult,
|
|
91
|
+
untested,
|
|
92
|
+
type UntestedResult,
|
|
93
|
+
} from './verbs.js';
|
|
66
94
|
|
|
67
95
|
const _HERE = fileURLToPath(import.meta.url);
|
|
68
96
|
export const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..', '..');
|
|
@@ -130,12 +158,33 @@ async function cmdBuild(argv: string[]): Promise<number> {
|
|
|
130
158
|
`✅ code-graph ${update ? 'updated' : 'built'} — ${fileCount} files · ${graph.nodes.length} nodes · ${graph.edges.length} edges\n` +
|
|
131
159
|
` languages: ${graph.languages.join(', ') || '(none)'} · grammar ABI ${graph.grammar_abi}\n` +
|
|
132
160
|
` edges: EXTRACTED ${c.EXTRACTED} · INFERRED ${c.INFERRED} · AMBIGUOUS ${c.AMBIGUOUS}\n` +
|
|
161
|
+
` resolved_via: ${resolvedViaHistogram(graph)}\n` +
|
|
133
162
|
(update ? ` incremental: ${reExtracted} re-extracted · ${reused} reused\n` : '') +
|
|
134
163
|
(out ? ` cache: ${path.relative(REPO_ROOT, out)}\n` : ''),
|
|
135
164
|
);
|
|
136
165
|
return 0;
|
|
137
166
|
}
|
|
138
167
|
|
|
168
|
+
/**
|
|
169
|
+
* The `resolved_via` histogram, descending by count.
|
|
170
|
+
*
|
|
171
|
+
* Printed on every build because it is the number that says whether the graph
|
|
172
|
+
* is getting BETTER, and the confidence split does not: 2.3's whole target is
|
|
173
|
+
* moving edges off `name-lookup` onto a real binding, and a reader who cannot
|
|
174
|
+
* see the starting distribution cannot tell whether it moved. Only non-zero
|
|
175
|
+
* buckets appear — a row of zeroes for the four mechanisms nothing emits yet
|
|
176
|
+
* would be noise on every build.
|
|
177
|
+
*/
|
|
178
|
+
function resolvedViaHistogram(graph: CodeGraph): string {
|
|
179
|
+
const counts = new Map<string, number>();
|
|
180
|
+
for (const e of graph.edges) counts.set(e.resolved_via, (counts.get(e.resolved_via) ?? 0) + 1);
|
|
181
|
+
if (counts.size === 0) return '(no edges)';
|
|
182
|
+
return [...counts.entries()]
|
|
183
|
+
.sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : 1))
|
|
184
|
+
.map(([via, n]) => `${via} ${n}`)
|
|
185
|
+
.join(' · ');
|
|
186
|
+
}
|
|
187
|
+
|
|
139
188
|
function cmdValidate(argv: string[]): number {
|
|
140
189
|
const p = flag(argv, '--graph') ?? DEFAULT_CACHE;
|
|
141
190
|
if (!fs.existsSync(p)) {
|
|
@@ -150,13 +199,57 @@ function cmdValidate(argv: string[]): number {
|
|
|
150
199
|
return 1;
|
|
151
200
|
}
|
|
152
201
|
const r = validateGraph(parsed);
|
|
153
|
-
if (r.ok) {
|
|
154
|
-
process.
|
|
155
|
-
|
|
202
|
+
if (!r.ok) {
|
|
203
|
+
process.stderr.write(`❌ graph schema invalid (${r.errors.length}):\n`);
|
|
204
|
+
for (const e of r.errors) process.stderr.write(` ${e}\n`);
|
|
205
|
+
return 1;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Twin ⇔ JSON, by checksum (road-to-a-graph-that-is-shipped 2.1).
|
|
209
|
+
//
|
|
210
|
+
// The schema check above says the JSON is well-formed. It says nothing
|
|
211
|
+
// about the derived store that now ANSWERS most queries, and since 2.1 the
|
|
212
|
+
// twin is read as an index rather than replayed as a blob — so a twin that
|
|
213
|
+
// disagreed with the JSON would produce different answers rather than a
|
|
214
|
+
// parse error. The comparison is `source_checksum`, which is the JSON's own
|
|
215
|
+
// content-addressed cache key, so agreement here means the twin was emitted
|
|
216
|
+
// from THIS graph and not merely from a graph.
|
|
217
|
+
const twinState = twinChecksumState(p, (parsed as CodeGraph).source_checksum);
|
|
218
|
+
const rel = path.relative(REPO_ROOT, p);
|
|
219
|
+
if (twinState.kind === 'mismatch') {
|
|
220
|
+
process.stderr.write(
|
|
221
|
+
`❌ SQLite twin disagrees with the canonical JSON: twin ${twinState.twin} ≠ json ${twinState.json}\n` +
|
|
222
|
+
' The twin is derived and disposable — delete it and re-run `code-graph build`.\n' +
|
|
223
|
+
` ${path.relative(REPO_ROOT, sqliteTwinPath(p))}\n`,
|
|
224
|
+
);
|
|
225
|
+
return 1;
|
|
226
|
+
}
|
|
227
|
+
process.stdout.write(`✅ graph schema valid: ${rel} · twin: ${twinState.kind}\n`);
|
|
228
|
+
return 0;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Compare the twin's stored `source_checksum` against the canonical JSON's.
|
|
233
|
+
*
|
|
234
|
+
* `absent` covers every state in which there is nothing to disagree WITH — no
|
|
235
|
+
* twin, no `node:sqlite`, a stale or corrupt twin the reader would refuse
|
|
236
|
+
* anyway. Those are not validation failures: the JSON is canonical and a
|
|
237
|
+
* missing accelerator changes no answer. Only a twin that is present, fresh,
|
|
238
|
+
* readable AND carrying a different checksum is a real contradiction.
|
|
239
|
+
*/
|
|
240
|
+
function twinChecksumState(
|
|
241
|
+
jsonPath: string,
|
|
242
|
+
jsonChecksum: string,
|
|
243
|
+
): { kind: 'absent' | 'match' } | { kind: 'mismatch'; twin: string; json: string } {
|
|
244
|
+
const index = openGraphIndex(jsonPath);
|
|
245
|
+
if (!index) return { kind: 'absent' };
|
|
246
|
+
try {
|
|
247
|
+
const twin = index.sourceChecksum;
|
|
248
|
+
if (twin && twin !== jsonChecksum) return { kind: 'mismatch', twin, json: jsonChecksum };
|
|
249
|
+
return { kind: 'match' };
|
|
250
|
+
} finally {
|
|
251
|
+
index.close();
|
|
156
252
|
}
|
|
157
|
-
process.stderr.write(`❌ graph schema invalid (${r.errors.length}):\n`);
|
|
158
|
-
for (const e of r.errors) process.stderr.write(` ${e}\n`);
|
|
159
|
-
return 1;
|
|
160
253
|
}
|
|
161
254
|
|
|
162
255
|
function resolveGraph(argv: string[]): { g: LoadedGraph; note: string } | { err: string } {
|
|
@@ -191,10 +284,18 @@ function budgetOf(argv: string[]): number {
|
|
|
191
284
|
return Number.isFinite(b) && b > 0 ? b : 1500;
|
|
192
285
|
}
|
|
193
286
|
|
|
194
|
-
|
|
195
|
-
|
|
287
|
+
/**
|
|
288
|
+
* Files changed between `ref` and `HEAD`, repo-relative.
|
|
289
|
+
*
|
|
290
|
+
* Extracted from `changedNodeSeeds` so the Phase-3 verbs seed from the same
|
|
291
|
+
* diff reading as `affected --since`. `null` distinguishes "git refused" (a bad
|
|
292
|
+
* rev, not a repository) from "nothing changed" — a verb that reports an empty
|
|
293
|
+
* impact set for a rev git could not resolve is asserting something it never
|
|
294
|
+
* measured.
|
|
295
|
+
*/
|
|
296
|
+
export function changedFiles(root: string, ref: string): string[] | null {
|
|
196
297
|
try {
|
|
197
|
-
|
|
298
|
+
return execFileSync('git', ['-C', root, 'diff', '--name-only', `${ref}..HEAD`], {
|
|
198
299
|
env: hardenedSpawnEnv(),
|
|
199
300
|
encoding: 'utf-8',
|
|
200
301
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
@@ -203,10 +304,16 @@ function changedNodeSeeds(g: LoadedGraph, root: string, ref: string): string[] {
|
|
|
203
304
|
.map((s) => s.trim())
|
|
204
305
|
.filter(Boolean);
|
|
205
306
|
} catch {
|
|
206
|
-
return
|
|
307
|
+
return null;
|
|
207
308
|
}
|
|
208
|
-
|
|
209
|
-
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function changedNodeSeeds(g: LoadedGraph, root: string, ref: string): string[] {
|
|
312
|
+
const files = changedFiles(root, ref);
|
|
313
|
+
if (files === null) return [];
|
|
314
|
+
// Served by the `nodes_source_file` index on the SQLite path, so a wide
|
|
315
|
+
// diff does not force the whole node table into memory to answer it.
|
|
316
|
+
return g.idsInFiles(files);
|
|
210
317
|
}
|
|
211
318
|
|
|
212
319
|
function cmdQuery(kind: 'query' | 'explain' | 'affected' | 'path', argv: string[]): number {
|
|
@@ -254,6 +361,213 @@ function isFlagValue(argv: string[], token: string): boolean {
|
|
|
254
361
|
return i > 0 && (argv[i - 1] as string).startsWith('--');
|
|
255
362
|
}
|
|
256
363
|
|
|
364
|
+
// Phase-3 gate verbs (3.1-3.3).
|
|
365
|
+
|
|
366
|
+
/** The staleness token every gate verb prints, per step 3.1–3.3's shared
|
|
367
|
+
* verify: "each prints `resolved_via` counts and the staleness state". */
|
|
368
|
+
/**
|
|
369
|
+
* The staleness of the graph the verb is ACTUALLY reading.
|
|
370
|
+
*
|
|
371
|
+
* `answeredBy` is `resolveGraph`'s own picked path when it has one. Without it
|
|
372
|
+
* the two disagreed on the case `resolveRoot`'s docstring exists for: with
|
|
373
|
+
* `--root <other repo>` the verb answers from `DEFAULT_CACHE` (anchored to the
|
|
374
|
+
* package tree) while `graphState` looked under the OTHER root, so the printed
|
|
375
|
+
* staleness described a cache the answer never came from. An independent review
|
|
376
|
+
* found the `--graph` half fixed and this half open.
|
|
377
|
+
*/
|
|
378
|
+
function stateOf(argv: string[], answeredBy?: string): GraphState {
|
|
379
|
+
const explicit = flag(argv, '--graph');
|
|
380
|
+
return graphState(resolveRoot(argv), explicit ?? answeredBy);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/** The two histogram lines + the staleness line, in one place so the four
|
|
384
|
+
* verbs cannot drift apart in how they report their own filtering. */
|
|
385
|
+
function renderEnvelope(r: { source: string; state: GraphState; accepted_via: string; rejected_via: string }): void {
|
|
386
|
+
process.stdout.write(`source: ${r.source}\nstaleness: ${r.state}\n`);
|
|
387
|
+
process.stdout.write(`accepted resolved_via: ${r.accepted_via}\n`);
|
|
388
|
+
process.stdout.write(`rejected resolved_via: ${r.rejected_via}\n`);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function renderReads(reads: readonly { path: string; lines: [number, number] | null }[]): void {
|
|
392
|
+
if (!reads.length) return;
|
|
393
|
+
process.stdout.write('minimal read set:\n');
|
|
394
|
+
for (const r of reads) process.stdout.write(` ${r.path}${r.lines ? `:${r.lines[0]}-${r.lines[1]}` : ''}\n`);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* `impact --diff <rev>` (3.1) — callers, dependents and test files reachable
|
|
399
|
+
* from the changed symbols over ACCEPTED edges, plus the producing edges and a
|
|
400
|
+
* minimal read set.
|
|
401
|
+
*/
|
|
402
|
+
function cmdImpact(argv: string[]): number {
|
|
403
|
+
const ref = flag(argv, '--diff');
|
|
404
|
+
if (ref === null) {
|
|
405
|
+
process.stderr.write('usage: code_graph/cli.ts impact --diff <rev> [--graph P] [--depth N]\n');
|
|
406
|
+
return 2;
|
|
407
|
+
}
|
|
408
|
+
const root = resolveRoot(argv);
|
|
409
|
+
const files = changedFiles(root, ref);
|
|
410
|
+
if (files === null) {
|
|
411
|
+
process.stderr.write(`code-graph: cannot resolve rev '${ref}' in ${root}\n`);
|
|
412
|
+
return 1;
|
|
413
|
+
}
|
|
414
|
+
const r = resolveGraph(argv);
|
|
415
|
+
if ('err' in r) {
|
|
416
|
+
process.stderr.write(`code-graph: ${r.err}\n`);
|
|
417
|
+
return 1;
|
|
418
|
+
}
|
|
419
|
+
try {
|
|
420
|
+
const depth = Number(flag(argv, '--depth'));
|
|
421
|
+
const res: ImpactResult = impact(
|
|
422
|
+
r.g,
|
|
423
|
+
files,
|
|
424
|
+
stateOf(argv, r.g.answeredBy),
|
|
425
|
+
Number.isFinite(depth) && depth > 0 ? depth : 2,
|
|
426
|
+
isTestFile,
|
|
427
|
+
);
|
|
428
|
+
renderEnvelope(res);
|
|
429
|
+
process.stdout.write(`changed files: ${files.length} · seeds: ${res.seeds.length}\n`);
|
|
430
|
+
if (res.unresolved_files.length)
|
|
431
|
+
process.stdout.write(`unresolved changed files (no node in the graph): ${res.unresolved_files.join(', ')}\n`);
|
|
432
|
+
process.stdout.write(`dependents (${res.dependents.length}):\n`);
|
|
433
|
+
for (const d of res.dependents) process.stdout.write(` ${d}\n`);
|
|
434
|
+
process.stdout.write(`test files (${res.test_files.length}):\n`);
|
|
435
|
+
for (const t of res.test_files) process.stdout.write(` ${t}\n`);
|
|
436
|
+
process.stdout.write(`producing edges (${res.producing_edges.length}):\n`);
|
|
437
|
+
for (const e of res.producing_edges) process.stdout.write(` ${e}\n`);
|
|
438
|
+
renderReads(res.recommended_reads);
|
|
439
|
+
return 0;
|
|
440
|
+
} finally {
|
|
441
|
+
r.g.close();
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/** `tests-for <symbol>` (3.2) — the test files that import the symbol. */
|
|
446
|
+
function cmdTestsFor(argv: string[]): number {
|
|
447
|
+
const positional = argv.filter((a) => !a.startsWith('--') && !isFlagValue(argv, a));
|
|
448
|
+
const symbol = positional[0];
|
|
449
|
+
if (!symbol) {
|
|
450
|
+
process.stderr.write('usage: code_graph/cli.ts tests-for <symbol> [--graph P]\n');
|
|
451
|
+
return 2;
|
|
452
|
+
}
|
|
453
|
+
const r = resolveGraph(argv);
|
|
454
|
+
if ('err' in r) {
|
|
455
|
+
process.stderr.write(`code-graph: ${r.err}\n`);
|
|
456
|
+
return 1;
|
|
457
|
+
}
|
|
458
|
+
try {
|
|
459
|
+
const res: TestsForResult = testsFor(r.g, symbol, stateOf(argv, r.g.answeredBy));
|
|
460
|
+
renderEnvelope(res);
|
|
461
|
+
process.stdout.write(`seeds: ${res.seeds.join(', ') || '(none)'}\n`);
|
|
462
|
+
process.stdout.write(`tests (${res.tests.length}):\n`);
|
|
463
|
+
for (const t of res.tests) process.stdout.write(` ${t}\n`);
|
|
464
|
+
renderReads(res.recommended_reads);
|
|
465
|
+
return 0;
|
|
466
|
+
} finally {
|
|
467
|
+
r.g.close();
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/** `untested --diff <rev>` (3.2) — changed symbols no test file imports. */
|
|
472
|
+
function cmdUntested(argv: string[]): number {
|
|
473
|
+
const ref = flag(argv, '--diff');
|
|
474
|
+
if (ref === null) {
|
|
475
|
+
process.stderr.write('usage: code_graph/cli.ts untested --diff <rev> [--graph P]\n');
|
|
476
|
+
return 2;
|
|
477
|
+
}
|
|
478
|
+
const root = resolveRoot(argv);
|
|
479
|
+
const files = changedFiles(root, ref);
|
|
480
|
+
if (files === null) {
|
|
481
|
+
process.stderr.write(`code-graph: cannot resolve rev '${ref}' in ${root}\n`);
|
|
482
|
+
return 1;
|
|
483
|
+
}
|
|
484
|
+
const r = resolveGraph(argv);
|
|
485
|
+
if ('err' in r) {
|
|
486
|
+
process.stderr.write(`code-graph: ${r.err}\n`);
|
|
487
|
+
return 1;
|
|
488
|
+
}
|
|
489
|
+
try {
|
|
490
|
+
const res: UntestedResult = untested(r.g, files, stateOf(argv, r.g.answeredBy));
|
|
491
|
+
renderEnvelope(res);
|
|
492
|
+
process.stdout.write(`changed files: ${files.length} · changed symbols: ${res.tested.length + res.untested.length}\n`);
|
|
493
|
+
if (res.unresolved_files.length)
|
|
494
|
+
process.stdout.write(`unresolved changed files (no node in the graph): ${res.unresolved_files.join(', ')}\n`);
|
|
495
|
+
process.stdout.write(`tested (${res.tested.length}) · untested (${res.untested.length}):\n`);
|
|
496
|
+
for (const u of res.untested) process.stdout.write(` ${u}\n`);
|
|
497
|
+
renderReads(res.recommended_reads);
|
|
498
|
+
return 0;
|
|
499
|
+
} finally {
|
|
500
|
+
r.g.close();
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* `dead` (3.3) — symbols with zero accepted in-edges that no declared entry
|
|
506
|
+
* point names. REFUSES by default when an entry-point source is unavailable;
|
|
507
|
+
* see `verbs.ts::dead` for why that refusal is not relaxable by an options bag.
|
|
508
|
+
*/
|
|
509
|
+
function cmdDead(argv: string[]): number {
|
|
510
|
+
const r = resolveGraph(argv);
|
|
511
|
+
if ('err' in r) {
|
|
512
|
+
process.stderr.write(`code-graph: ${r.err}\n`);
|
|
513
|
+
return 1;
|
|
514
|
+
}
|
|
515
|
+
try {
|
|
516
|
+
const root = resolveRoot(argv);
|
|
517
|
+
const sources = repoEntryPoints(root);
|
|
518
|
+
const listFile = flag(argv, '--entry-points');
|
|
519
|
+
if (listFile !== null) {
|
|
520
|
+
if (!fs.existsSync(listFile)) {
|
|
521
|
+
process.stderr.write(`code-graph: entry-point list not found: ${listFile}\n`);
|
|
522
|
+
return 1;
|
|
523
|
+
}
|
|
524
|
+
// An operator-supplied list SUPPLIES the missing source, so the
|
|
525
|
+
// `exports` row stops being a gap rather than being ignored: the
|
|
526
|
+
// caller has stated the entry points the graph could not.
|
|
527
|
+
const supplied = entryPointsFromFile(listFile);
|
|
528
|
+
const patched = sources.map((srcRow) =>
|
|
529
|
+
srcRow.name === 'exports'
|
|
530
|
+
? { ...srcRow, status: 'read' as const, detail: `supplied by ${listFile}`, entries: [] }
|
|
531
|
+
: srcRow,
|
|
532
|
+
);
|
|
533
|
+
patched.push(supplied);
|
|
534
|
+
const res: DeadResult = dead(r.g, patched, stateOf(argv, r.g.answeredBy));
|
|
535
|
+
return renderDead(res);
|
|
536
|
+
}
|
|
537
|
+
const res: DeadResult = dead(r.g, sources, stateOf(argv, r.g.answeredBy), {
|
|
538
|
+
acceptMissingExports: argv.includes('--accept-missing-exports'),
|
|
539
|
+
});
|
|
540
|
+
return renderDead(res);
|
|
541
|
+
} finally {
|
|
542
|
+
r.g.close();
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function renderDead(res: DeadResult): number {
|
|
547
|
+
// The envelope FIRST, on both branches. It used to print only after the
|
|
548
|
+
// refusal check, so the default invocation in this repository — where
|
|
549
|
+
// `exports` is always unavailable — printed neither histogram nor the
|
|
550
|
+
// staleness line, and 3.1-3.3's shared verify ("each prints `resolved_via`
|
|
551
|
+
// counts and the staleness state") held only on the branch that answers.
|
|
552
|
+
// A reader of a refusal wants the staleness most of all: it says whether
|
|
553
|
+
// re-running after a rebuild could change anything.
|
|
554
|
+
renderEnvelope(res);
|
|
555
|
+
process.stdout.write('entry-point sources consulted:\n');
|
|
556
|
+
for (const srcRow of res.sources)
|
|
557
|
+
process.stdout.write(
|
|
558
|
+
` ${srcRow.name.padEnd(13)} ${srcRow.status}${srcRow.detail ? ` — ${srcRow.detail}` : ''} · ${srcRow.entries.length} entr${srcRow.entries.length === 1 ? 'y' : 'ies'}\n`,
|
|
559
|
+
);
|
|
560
|
+
if (res.refusal !== null) {
|
|
561
|
+
process.stderr.write(`❌ dead refused: ${res.refusal}\n`);
|
|
562
|
+
return 1;
|
|
563
|
+
}
|
|
564
|
+
process.stdout.write(`excluded as declared entry points: ${res.excluded.length}\n`);
|
|
565
|
+
process.stdout.write(`dead (${res.dead.length}):\n`);
|
|
566
|
+
for (const d of res.dead) process.stdout.write(` ${d}\n`);
|
|
567
|
+
renderReads(res.recommended_reads);
|
|
568
|
+
return 0;
|
|
569
|
+
}
|
|
570
|
+
|
|
257
571
|
function cmdDetect(argv: string[]): number {
|
|
258
572
|
const root = resolveRoot(argv);
|
|
259
573
|
if (flag(argv, '--format') === 'json') {
|
|
@@ -394,9 +708,14 @@ export async function main(argv: string[] = process.argv.slice(2)): Promise<numb
|
|
|
394
708
|
if (sub === 'refresh') return cmdRefresh(argv.slice(1));
|
|
395
709
|
if (sub === 'query' || sub === 'explain' || sub === 'affected' || sub === 'path')
|
|
396
710
|
return cmdQuery(sub, argv.slice(1));
|
|
711
|
+
if (sub === 'impact') return cmdImpact(argv.slice(1));
|
|
712
|
+
if (sub === 'tests-for') return cmdTestsFor(argv.slice(1));
|
|
713
|
+
if (sub === 'untested') return cmdUntested(argv.slice(1));
|
|
714
|
+
if (sub === 'dead') return cmdDead(argv.slice(1));
|
|
397
715
|
if (sub === 'suggest-verb') return cmdSuggestVerb(argv.slice(1));
|
|
398
716
|
process.stderr.write(
|
|
399
|
-
'usage: code_graph/cli.ts <build|validate|detect|refresh|query|explain|affected|path|
|
|
717
|
+
'usage: code_graph/cli.ts <build|validate|detect|refresh|query|explain|affected|path|' +
|
|
718
|
+
'impact|tests-for|untested|dead|suggest-verb> [options]\n',
|
|
400
719
|
);
|
|
401
720
|
return 2;
|
|
402
721
|
} catch (e) {
|
|
@@ -184,3 +184,41 @@ export function computeVerdict(root: string, nativeCache: string): VerdictJSON {
|
|
|
184
184
|
sources,
|
|
185
185
|
};
|
|
186
186
|
}
|
|
187
|
+
|
|
188
|
+
/** Cache path a repository root's native graph lives at, repo-relative. */
|
|
189
|
+
export const NATIVE_CACHE_REL = path.join('agents', 'runtime', 'state', 'code-graph-v1.json');
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* The three-state staleness token every consumer of the graph reports.
|
|
193
|
+
*
|
|
194
|
+
* Lives HERE rather than in the PreToolUse hook that first needed it
|
|
195
|
+
* (`hooks/code_graph_context_hook.ts`, step 1.2). Phase 3's verbs must each
|
|
196
|
+
* print the staleness state, and an engine module importing a hook module to
|
|
197
|
+
* learn it would invert the dependency direction D9 measures — the hook imports
|
|
198
|
+
* the engine, never the other way round. The hook re-exports this so its own
|
|
199
|
+
* consumers are unaffected.
|
|
200
|
+
*/
|
|
201
|
+
export type GraphState = 'absent' | 'fresh' | `behind:${number}`;
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Resolve the graph's state for `root`.
|
|
205
|
+
*
|
|
206
|
+
* `absent` means no source at all — the silent case. A picked source whose
|
|
207
|
+
* staleness is UNKNOWN reads as `fresh`, mirroring {@link computeVerdict}'s own
|
|
208
|
+
* `picked.stale ? STALE : FRESH`: unknown is not treated as stale, because
|
|
209
|
+
* inventing a commit count would be worse than reporting none.
|
|
210
|
+
*
|
|
211
|
+
* `nativeCache` overrides where the native graph is looked for. It exists
|
|
212
|
+
* because a verb invoked with an explicit `--graph <path>` would otherwise
|
|
213
|
+
* report `absent` while answering from that very file — the state of a cache
|
|
214
|
+
* nobody asked about. Passing the graph the verb is actually reading makes the
|
|
215
|
+
* printed staleness a statement about the answer rather than about a
|
|
216
|
+
* convention.
|
|
217
|
+
*/
|
|
218
|
+
export function graphState(root: string, nativeCache?: string): GraphState {
|
|
219
|
+
const picked = pickSource(detectSources(root, nativeCache ?? path.join(root, NATIVE_CACHE_REL)));
|
|
220
|
+
if (!picked) return 'absent';
|
|
221
|
+
if (picked.stale !== true) return 'fresh';
|
|
222
|
+
const behind = picked.commits_behind;
|
|
223
|
+
return `behind:${typeof behind === 'number' ? behind : 0}`;
|
|
224
|
+
}
|
|
@@ -67,6 +67,16 @@ export interface RawEdge {
|
|
|
67
67
|
* searched for the remote one.
|
|
68
68
|
*/
|
|
69
69
|
localName?: string;
|
|
70
|
+
/**
|
|
71
|
+
* `imports` only, PHP: the FULLY-QUALIFIED name as written (`App\\Svc\\Mailer`).
|
|
72
|
+
*
|
|
73
|
+
* `targetName` carries only the base name, because that is all the
|
|
74
|
+
* repo-wide lookup could use. Composer PSR-4 maps the NAMESPACE to a
|
|
75
|
+
* directory, so it needs the part `baseName()` discards — the same shape as
|
|
76
|
+
* `moduleSpecifier` for TS: the one piece of evidence that says where the
|
|
77
|
+
* name comes from, and it is right there in the source text.
|
|
78
|
+
*/
|
|
79
|
+
fqName?: string;
|
|
70
80
|
}
|
|
71
81
|
|
|
72
82
|
export interface FileExtract {
|
|
@@ -137,6 +147,7 @@ function extractPhp(root: TsNode, file: string, out: FileExtract): void {
|
|
|
137
147
|
relation: 'imports',
|
|
138
148
|
targetName: baseName(nameNode.text),
|
|
139
149
|
confidenceHint: 'EXTRACTED',
|
|
150
|
+
fqName: nameNode.text,
|
|
140
151
|
});
|
|
141
152
|
}
|
|
142
153
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Report what the PRODUCTION loader makes of each grammar file given as an
|
|
4
|
+
* argument, as one line of JSON on stdout.
|
|
5
|
+
*
|
|
6
|
+
* A separate process because `check_pack_size.main` is synchronous while
|
|
7
|
+
* `Language.load` is not, and because keeping the probe out of the gate's own
|
|
8
|
+
* process means the gate cannot accidentally hold a loaded grammar's emscripten
|
|
9
|
+
* heap for the rest of its run.
|
|
10
|
+
*
|
|
11
|
+
* Never throws for a bad file: an unreadable or invalid grammar comes back as
|
|
12
|
+
* `{ ok: false, error }`, because "the loader refused this" is the ANSWER the
|
|
13
|
+
* eligibility predicate needs, not a crash it has to interpret.
|
|
14
|
+
*/
|
|
15
|
+
import process from 'node:process';
|
|
16
|
+
|
|
17
|
+
import { probeGrammarFile } from './loader.js';
|
|
18
|
+
|
|
19
|
+
interface ProbeRow {
|
|
20
|
+
path: string;
|
|
21
|
+
ok: boolean;
|
|
22
|
+
abi?: number;
|
|
23
|
+
grammar_id?: string;
|
|
24
|
+
error?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function main(): Promise<number> {
|
|
28
|
+
const rows: ProbeRow[] = [];
|
|
29
|
+
for (const p of process.argv.slice(2)) {
|
|
30
|
+
try {
|
|
31
|
+
const r = await probeGrammarFile(p);
|
|
32
|
+
rows.push({ path: p, ok: true, abi: r.abi, grammar_id: r.grammar_id });
|
|
33
|
+
} catch (e) {
|
|
34
|
+
rows.push({ path: p, ok: false, error: (e as Error).message });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
process.stdout.write(`${JSON.stringify(rows)}\n`);
|
|
38
|
+
return 0;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
void main().then((c) => {
|
|
42
|
+
process.exitCode = c;
|
|
43
|
+
});
|
|
@@ -2,19 +2,29 @@
|
|
|
2
2
|
* WASM tree-sitter loader for the code-graph engine.
|
|
3
3
|
*
|
|
4
4
|
* Pinned pair (ABI-verified 2026-07-23): `web-tree-sitter@0.24.7` +
|
|
5
|
-
*
|
|
6
|
-
* `tests/scripts/code_graph.test.ts
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
5
|
+
* grammar ABI 14. The ABI smoke test lives in
|
|
6
|
+
* `tests/scripts/code_graph.test.ts`.
|
|
7
|
+
*
|
|
8
|
+
* SHIPPED TO CONSUMERS (ADR-259, amended 2026-09-07). `web-tree-sitter` is a
|
|
9
|
+
* runtime `dependency`, and the three loadable grammars are vendored at
|
|
10
|
+
* `src/vendor/grammars/` and listed in `package.json` `files`. So a consumer
|
|
11
|
+
* install resolves both halves with no manual step. `tree-sitter-wasms` stays a
|
|
12
|
+
* devDependency — it is the source the vendored copies are refreshed from, and
|
|
13
|
+
* the fallback below keeps this repo's own dev flow (and any consumer holding
|
|
14
|
+
* the full 36-grammar pack) working unchanged.
|
|
15
|
+
*
|
|
16
|
+
* No network, no node-gyp, and no runtime grammar download — Kill register K1:
|
|
17
|
+
* both lookups below are filesystem reads of already-installed bytes.
|
|
10
18
|
*
|
|
11
19
|
* web-tree-sitter 0.24.x is CJS with a single default export (the Parser
|
|
12
20
|
* class) and the classic `Parser.Language.load()` API; we reach it through
|
|
13
21
|
* `createRequire` so this ESM module stays portable under the repo's tsx
|
|
14
22
|
* runner.
|
|
15
23
|
*/
|
|
24
|
+
import * as fs from 'node:fs';
|
|
16
25
|
import { createRequire } from 'node:module';
|
|
17
26
|
import * as path from 'node:path';
|
|
27
|
+
import { fileURLToPath } from 'node:url';
|
|
18
28
|
|
|
19
29
|
import { EXPECTED_GRAMMAR_ABI, GRAMMAR_WASM, type Lang } from './types.js';
|
|
20
30
|
|
|
@@ -53,22 +63,35 @@ interface ParserStatic {
|
|
|
53
63
|
}
|
|
54
64
|
|
|
55
65
|
/**
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
* `check_dependency_floors` only
|
|
66
|
+
* Reached only when BOTH grammar sources are missing — the vendored set that
|
|
67
|
+
* ships in the tarball and the devDependency pack. That is a broken or
|
|
68
|
+
* partially-extracted install, not the ordinary consumer state, so the message
|
|
69
|
+
* says how to repair rather than how to opt in.
|
|
70
|
+
*
|
|
71
|
+
* It still carries the exact pin because the pair is ABI-coupled and
|
|
72
|
+
* `check_dependency_floors` reads only version ranges, never this string.
|
|
63
73
|
*/
|
|
64
74
|
const INSTALL_HINT =
|
|
65
|
-
'the code-graph engine
|
|
66
|
-
'
|
|
75
|
+
'the code-graph engine could not find its grammars. They ship with this package at ' +
|
|
76
|
+
'`src/vendor/grammars/`, so this usually means an incomplete install — reinstall the ' +
|
|
77
|
+
'package first. To supply them manually, install the ABI-locked pack:\n' +
|
|
67
78
|
' npm i web-tree-sitter@0.24.7 tree-sitter-wasms@0.1.13\n' +
|
|
68
79
|
'Both versions are exact on purpose — the pair is ABI-coupled (grammar ABI 14) ' +
|
|
69
80
|
'and must move together.';
|
|
70
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Directory holding the grammars that ship inside the package.
|
|
84
|
+
*
|
|
85
|
+
* `../../..` from `src/scripts/code_graph/` is the package root — the same
|
|
86
|
+
* walk `_lib/agent_settings.ts` uses from `src/scripts/_lib/`. A wrong answer
|
|
87
|
+
* here is not a failure: `grammarWasmPath` only uses this path when the file is
|
|
88
|
+
* actually present, and otherwise falls through to the devDependency pack,
|
|
89
|
+
* which is exactly today's behaviour.
|
|
90
|
+
*/
|
|
91
|
+
const VENDORED_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', 'vendor', 'grammars');
|
|
92
|
+
|
|
71
93
|
let _wasmDir: string | null = null;
|
|
94
|
+
/** The devDependency grammar pack — the fallback source, and this repo's own. */
|
|
72
95
|
function wasmDir(): string {
|
|
73
96
|
if (_wasmDir === null) {
|
|
74
97
|
let pkgJson: string;
|
|
@@ -86,7 +109,18 @@ let _initialized = false;
|
|
|
86
109
|
const _langCache = new Map<Lang, TsLanguage>();
|
|
87
110
|
const _parserCache = new Map<Lang, TsParser>();
|
|
88
111
|
|
|
112
|
+
/**
|
|
113
|
+
* Absolute path to a grammar, vendored set first.
|
|
114
|
+
*
|
|
115
|
+
* Order is load-bearing: the vendored set is what a consumer actually has, and
|
|
116
|
+
* it is ABI-pinned by the same change that wrote it. The devDependency pack is
|
|
117
|
+
* second so this repo keeps working when `src/vendor/grammars/` has not been
|
|
118
|
+
* refreshed, and so a consumer holding the full 36-grammar pack can reach a
|
|
119
|
+
* grammar the vendored three do not cover.
|
|
120
|
+
*/
|
|
89
121
|
export function grammarWasmPath(lang: Lang): string {
|
|
122
|
+
const vendored = path.join(VENDORED_DIR, GRAMMAR_WASM[lang]);
|
|
123
|
+
if (fs.existsSync(vendored)) return vendored;
|
|
90
124
|
return path.join(wasmDir(), GRAMMAR_WASM[lang]);
|
|
91
125
|
}
|
|
92
126
|
|
|
@@ -150,3 +184,43 @@ export async function getParser(lang: Lang): Promise<TsParser> {
|
|
|
150
184
|
export function nodeHasError(n: TsNode): boolean {
|
|
151
185
|
return typeof n.hasError === 'function' ? n.hasError() : n.hasError;
|
|
152
186
|
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Load an ARBITRARY grammar file through the production loader and report what
|
|
190
|
+
* it actually is.
|
|
191
|
+
*
|
|
192
|
+
* Exists for `check_pack_size`'s binary-eligibility predicate, and the "through
|
|
193
|
+
* the production loader" part is the whole point: the gate must not re-implement
|
|
194
|
+
* grammar loading, because a second implementation could accept bytes the engine
|
|
195
|
+
* rejects — which is precisely the gap that would let a broken grammar ship
|
|
196
|
+
* while the gate reported it valid. Same `_Parser()` instance, same
|
|
197
|
+
* `Language.load`, same ABI reading as `loadLanguage`.
|
|
198
|
+
*
|
|
199
|
+
* `grammar_id` comes from the module's exported `tree_sitter_<id>` symbol rather
|
|
200
|
+
* than from the `Language` object, because web-tree-sitter 0.24's `Language`
|
|
201
|
+
* exposes `version` and no name. `WebAssembly.Module.exports` is the runtime's
|
|
202
|
+
* own parser, so the id is read rather than guessed.
|
|
203
|
+
*
|
|
204
|
+
* Throws on anything it cannot establish — a caller that wants a verdict rather
|
|
205
|
+
* than an exception should catch. Never silently returns a partial answer.
|
|
206
|
+
*/
|
|
207
|
+
export async function probeGrammarFile(wasmPath: string): Promise<{ abi: number; grammar_id: string }> {
|
|
208
|
+
const bytes = fs.readFileSync(wasmPath);
|
|
209
|
+
const mod = new WebAssembly.Module(bytes);
|
|
210
|
+
// A grammar module exports `tree_sitter_<id>` plus, usually, a family of
|
|
211
|
+
// `tree_sitter_<id>_external_scanner_*` helpers — measured: 6 such exports
|
|
212
|
+
// on php, 7 on typescript and javascript. The language id is the one
|
|
213
|
+
// WITHOUT the scanner suffix, and filtering for it leaves exactly one.
|
|
214
|
+
const ids = WebAssembly.Module.exports(mod)
|
|
215
|
+
.map((e) => e.name)
|
|
216
|
+
.filter((n) => n.startsWith('tree_sitter_') && !n.includes('_external_scanner_'))
|
|
217
|
+
.map((n) => n.slice('tree_sitter_'.length));
|
|
218
|
+
if (ids.length !== 1) {
|
|
219
|
+
throw new Error(
|
|
220
|
+
`grammar probe: expected exactly one tree_sitter_* export in ${wasmPath}, found ${String(ids.length)}`,
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
await _init();
|
|
224
|
+
const language = await _Parser().Language.load(wasmPath);
|
|
225
|
+
return { abi: language.version, grammar_id: ids[0] as string };
|
|
226
|
+
}
|