@event4u/agent-config 14.17.0 → 14.19.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.
Files changed (184) hide show
  1. package/.augment-plugin/marketplace.json +3 -3
  2. package/.augment-plugin/plugin.json +1 -1
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/CHANGELOG.md +207 -77
  5. package/README.md +72 -150
  6. package/dist/agent-src/README.md +1 -1
  7. package/dist/agent-src/commands/agent-handoff.md +2 -2
  8. package/dist/agent-src/commands/pr/merge.md +35 -44
  9. package/dist/agent-src/commands/review/changes.md +19 -19
  10. package/dist/agent-src/commands/review.md +2 -2
  11. package/dist/agent-src/contexts/execution/host-capability-manifest.md +30 -3
  12. package/dist/agent-src/rules/autonomous-execution.md +1 -1
  13. package/dist/agent-src/rules/git-history-discipline.md +2 -2
  14. package/dist/agent-src/rules/guidelines.md +1 -1
  15. package/dist/agent-src/rules/session-canary.md +2 -2
  16. package/dist/agent-src/rules/token-efficiency.md +1 -1
  17. package/dist/agent-src/skills/accessibility-auditor/SKILL.md +2 -0
  18. package/dist/agent-src/skills/adversarial-review/SKILL.md +0 -8
  19. package/dist/agent-src/skills/ai-code-blindspots/SKILL.md +27 -1
  20. package/dist/agent-src/skills/canvas-design/SKILL.md +1 -1
  21. package/dist/agent-src/skills/code-intelligence/SKILL.md +60 -12
  22. package/dist/agent-src/skills/complexity-first-planning/SKILL.md +27 -0
  23. package/dist/agent-src/skills/design-intelligence/SKILL.md +1 -1
  24. package/dist/agent-src/skills/design-intelligence/data/app-interface.csv +1 -1
  25. package/dist/agent-src/skills/design-intelligence/data/motion.csv +17 -17
  26. package/dist/agent-src/skills/design-intelligence/data/ux-guidelines.csv +2 -2
  27. package/dist/agent-src/skills/design-intelligence/references/context-and-registers.md +17 -0
  28. package/dist/agent-src/skills/design-intelligence/references/design-rules-checklist.md +4 -4
  29. package/dist/agent-src/skills/design-review/SKILL.md +3 -1
  30. package/dist/agent-src/skills/design-variations/SKILL.md +1 -1
  31. package/dist/agent-src/skills/existing-ui-audit/SKILL.md +1 -1
  32. package/dist/agent-src/skills/fe-design/SKILL.md +32 -5
  33. package/dist/agent-src/skills/fe-design/references/craft-floor.md +7 -0
  34. package/dist/agent-src/skills/fe-design/references/design-patterns.md +47 -2
  35. package/dist/agent-src/skills/feature-planning/SKILL.md +34 -0
  36. package/dist/agent-src/skills/git-workflow/SKILL.md +37 -12
  37. package/dist/agent-src/skills/git-workflow/references/push-closes-its-loop.md +176 -0
  38. package/dist/agent-src/skills/laravel-mail/SKILL.md +44 -0
  39. package/dist/agent-src/skills/laravel-scheduling/SKILL.md +3 -1
  40. package/dist/agent-src/skills/project-analyzer/SKILL.md +0 -6
  41. package/dist/agent-src/skills/react-native-setup/SKILL.md +3 -3
  42. package/dist/agent-src/skills/react-shadcn-ui/SKILL.md +1 -1
  43. package/dist/agent-src/skills/sequential-thinking/SKILL.md +18 -8
  44. package/dist/agent-src/skills/tailwind-engineer/SKILL.md +1 -1
  45. package/dist/agent-src/skills/testing-anti-patterns/SKILL.md +40 -0
  46. package/dist/agent-src/skills/ui-apply-generic/SKILL.md +1 -1
  47. package/dist/agent-src/skills/ui-component-architect/SKILL.md +1 -1
  48. package/dist/agent-src/skills/using-git-worktrees/SKILL.md +29 -2
  49. package/dist/agent-src/skills/verify-repair-loop/SKILL.md +53 -8
  50. package/dist/agent-src/skills/worktree-lifecycle/SKILL.md +3 -1
  51. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  52. package/dist/cli/commands/recordTriggerEval.js +2 -2
  53. package/dist/cli/main.js +1 -1
  54. package/dist/cli-delegate/{chunk-AKJGUQNO.js → chunk-GZTMRYCN.js} +405 -341
  55. package/dist/cli-delegate/{chunk-7RXIKF62.js → chunk-IDHUQIXP.js} +283 -132
  56. package/dist/cli-delegate/cmd_adr_effective.js +318 -81
  57. package/dist/cli-delegate/cmd_conformance.js +749 -669
  58. package/dist/cli-delegate/cmd_doctor.js +1 -1
  59. package/dist/cli-delegate/cmd_export.js +1 -1
  60. package/dist/cli-delegate/cmd_handoff.js +1 -1
  61. package/dist/cli-delegate/cmd_prune.js +1 -1
  62. package/dist/cli-delegate/cmd_route_audit.js +1 -1
  63. package/dist/cli-delegate/cmd_settings_migrate.js +1 -1
  64. package/dist/cli-delegate/cmd_sync.js +1 -1
  65. package/dist/cli-delegate/cmd_uninstall.js +1 -1
  66. package/dist/cli-delegate/cmd_validate.js +1 -1
  67. package/dist/discovery/deprecation-report.md +1 -1
  68. package/dist/discovery/discovery-manifest.json +39 -38
  69. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  70. package/dist/discovery/discovery-manifest.summary.md +1 -1
  71. package/dist/discovery/dormancy-report.md +1 -1
  72. package/dist/discovery/orphan-report.md +1 -1
  73. package/dist/discovery/packs.json +2 -2
  74. package/dist/discovery/trust-report.md +1 -1
  75. package/dist/discovery/workspaces.json +2 -2
  76. package/dist/hooks/dispatch.js +2764 -2439
  77. package/dist/install/install.mjs +588 -527
  78. package/dist/mcp/registry-manifest.json +2 -2
  79. package/dist/mcp/server.json +2 -2
  80. package/dist/mcp/server.mjs +1381 -1138
  81. package/dist/scripts/_lib/model_tier.js +1 -1
  82. package/dist/shared/settingsConsent.js +1 -1
  83. package/docs/guidelines/agent-infra/evaluator-independence-mechanics.md +1 -1
  84. package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +1 -1
  85. package/docs/guidelines/design-antipatterns.md +87 -64
  86. package/package.json +1 -1
  87. package/src/agent-src/README.md +1 -1
  88. package/src/config/ci-local-parity.yml +6 -0
  89. package/src/config/conformance-claim-baseline.json +9 -0
  90. package/src/config/gate-coverage.yml +170 -0
  91. package/src/config/gate-violation-baselines.json +4 -4
  92. package/src/config/release-gate-locality.yml +141 -0
  93. package/src/scripts/_cli/cmd_doctor.ts +2 -2
  94. package/src/scripts/_cli/cmd_handoff.ts +1 -1
  95. package/src/scripts/_cli/cmd_update.ts +1 -1
  96. package/src/scripts/_cli/cmd_versions.ts +1 -1
  97. package/src/scripts/_lib/adr_frontmatter.ts +44 -0
  98. package/src/scripts/_lib/consequence_objects.ts +152 -0
  99. package/src/scripts/_lib/exec_evidence.ts +7 -0
  100. package/src/scripts/_lib/global_deploy_inventory.ts +2 -2
  101. package/src/scripts/_lib/host_permission_checks.ts +251 -0
  102. package/src/scripts/_lib/model_tier.ts +1 -1
  103. package/src/scripts/_lib/obligation_frequency.ts +2 -2
  104. package/src/scripts/_lib/release_highlights.ts +265 -1
  105. package/src/scripts/_lib/release_material.ts +129 -0
  106. package/src/scripts/_lib/repeated_run.ts +77 -0
  107. package/src/scripts/_lib/runtime_journal.ts +26 -3
  108. package/src/scripts/_lib/runtime_wiring_checks.ts +12 -1
  109. package/src/scripts/_lib/session_register.ts +11 -6
  110. package/src/scripts/_lib/sqlite_schema.ts +22 -0
  111. package/src/scripts/_lib/surface.ts +86 -0
  112. package/src/scripts/_lib/tamper_vocabulary.ts +242 -0
  113. package/src/scripts/_lib/ui_authority.ts +116 -0
  114. package/src/scripts/_lib/unified_diff.ts +121 -0
  115. package/src/scripts/adr/regenerate_index.ts +54 -1
  116. package/src/scripts/adr_cite_check.ts +71 -15
  117. package/src/scripts/ai_council/clients.ts +1 -1
  118. package/src/scripts/autonomy_friction_corpus.ts +229 -0
  119. package/src/scripts/build_proof.ts +15 -2
  120. package/src/scripts/check_beta_review_markers.ts +141 -17
  121. package/src/scripts/check_enforcement_coverage.ts +1 -1
  122. package/src/scripts/check_enforcement_denominator.ts +121 -11
  123. package/src/scripts/check_finding_dispositions.ts +239 -6
  124. package/src/scripts/check_gate_coverage.ts +96 -0
  125. package/src/scripts/check_installed_hooks_fresh.ts +381 -0
  126. package/src/scripts/check_release_highlights.ts +99 -14
  127. package/src/scripts/check_skill_activation_claim.ts +179 -0
  128. package/src/scripts/check_suppression_hygiene.ts +11 -0
  129. package/src/scripts/check_trigger_evals.ts +1 -1
  130. package/src/scripts/code_graph/build.ts +230 -33
  131. package/src/scripts/code_graph/extract.ts +168 -33
  132. package/src/scripts/code_graph/types.ts +37 -2
  133. package/src/scripts/code_graph/validate.ts +12 -1
  134. package/src/scripts/conformance_scan.ts +45 -4
  135. package/src/scripts/detect_silent_catch.ts +403 -0
  136. package/src/scripts/detect_verification_tampering.ts +538 -0
  137. package/src/scripts/explain_run.ts +1 -1
  138. package/src/scripts/generate_capability_matrix.ts +1 -1
  139. package/src/scripts/git_authorization_hook.ts +228 -18
  140. package/src/scripts/hook_manifest.json +1 -1
  141. package/src/scripts/hook_manifest.yaml +47 -30
  142. package/src/scripts/hooks/augment-dispatcher.sh +1 -1
  143. package/src/scripts/hooks/block_no_verify.ts +3 -3
  144. package/src/scripts/hooks/category_a.ts +291 -0
  145. package/src/scripts/hooks/cline-dispatcher.sh +1 -1
  146. package/src/scripts/hooks/concern_registry.ts +2 -2
  147. package/src/scripts/hooks/cursor-dispatcher.sh +11 -5
  148. package/src/scripts/hooks/dispatch_hook.ts +17 -22
  149. package/src/scripts/hooks/evidence_independence.ts +21 -2
  150. package/src/scripts/hooks/{block_unauthorized_git.ts → git_command_classifier.ts} +175 -364
  151. package/src/scripts/hooks/host_lowering.ts +215 -0
  152. package/src/scripts/hooks/host_lowering.yaml +185 -0
  153. package/src/scripts/hooks/host_semantics.ts +105 -37
  154. package/src/scripts/hooks/journal_record_hook.ts +10 -0
  155. package/src/scripts/hooks/permission_gate.ts +117 -0
  156. package/src/scripts/hooks/push_settle_hook.ts +250 -0
  157. package/src/scripts/hooks/ship_diff_volume_hook.ts +1 -1
  158. package/src/scripts/hooks_status.ts +12 -10
  159. package/src/scripts/install-hooks.sh +134 -1
  160. package/src/scripts/install.ts +53 -82
  161. package/src/scripts/install_anthropic_key.sh +2 -2
  162. package/src/scripts/language_mirror_hook.ts +27 -5
  163. package/src/scripts/lint_design_antipattern_parity.ts +80 -0
  164. package/src/scripts/lint_evidence_artifacts.ts +45 -1
  165. package/src/scripts/lint_hook_manifest.ts +87 -3
  166. package/src/scripts/lint_motion_authority_drift.ts +731 -0
  167. package/src/scripts/lint_readme_size.ts +158 -12
  168. package/src/scripts/lint_reasoning_fixtures.ts +216 -0
  169. package/src/scripts/lint_skill_trigger_contradiction.ts +161 -0
  170. package/src/scripts/measure_sibling_naming.ts +150 -0
  171. package/src/scripts/redact_hook_capture.ts +1 -1
  172. package/src/scripts/release.ts +31 -143
  173. package/src/scripts/release_drill.ts +71 -1
  174. package/src/scripts/release_publication.ts +243 -3
  175. package/src/scripts/release_verify.ts +220 -0
  176. package/src/scripts/report_skill_activation.ts +319 -7
  177. package/src/scripts/schemas/review-findings.schema.json +6 -1
  178. package/src/scripts/schemas/skill.schema.json +12 -2
  179. package/src/scripts/self_review_gate.ts +10 -1
  180. package/src/scripts/session_canary_hook.ts +1 -1
  181. package/src/scripts/session_register_hook.ts +4 -3
  182. package/src/scripts/trigger_eval_grandfather.json +5 -1
  183. package/src/scripts/validate_agent_settings.ts +1 -1
  184. package/src/shared/settingsConsent.ts +1 -1
@@ -0,0 +1,179 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * The published skill-activation figure agrees with the record it came from.
4
+ *
5
+ * WHY THIS IS A SEPARATE SCRIPT FROM THE CENSUS
6
+ * ---------------------------------------------
7
+ * `report_skill_activation` is a REPORT: it gates on nothing, has no threshold,
8
+ * and its own docstring says it must never acquire one. This is a GATE, and the
9
+ * two must not share an entry point — `src/config/gate-coverage.yml` is a
10
+ * manifest of gates, and `_lib/gate_population.ts` classifies a script by its
11
+ * prefix precisely so that "is this a gate" is answered structurally rather
12
+ * than by whoever last edited a config. Registering a `report_*` id there forks
13
+ * that population, which `tests/scripts/gate_population.test.ts` refuses.
14
+ *
15
+ * So the split is not cosmetic. The report keeps its exit-0 contract; the
16
+ * assertion that CAN fail lives here, where a floor and a self-test belong.
17
+ *
18
+ * WHAT IT CANNOT DO, STATED SO NOBODY READS IT AS COVERED
19
+ * ------------------------------------------------------
20
+ * It compares two COMMITTED files — the census record against the sentence
21
+ * `docs/CLAIMS.md` publishes. It cannot tell whether the record still describes
22
+ * the world: the transcript store it was taken from is one machine's, keyed on
23
+ * the directory the census ran in, and absent from CI. Re-taking the
24
+ * measurement is `report_skill_activation --emit`, which refuses to write from
25
+ * an empty store. This gate only forbids the published sentence and the record
26
+ * from telling two different stories.
27
+ *
28
+ * Exit: 0 agree · 1 disagree or a file is missing.
29
+ */
30
+
31
+ import * as fs from 'node:fs';
32
+ import * as os from 'node:os';
33
+ import * as path from 'node:path';
34
+ import { fileURLToPath, pathToFileURL } from 'node:url';
35
+
36
+ import { runGateCli, runSelfTest } from './_lib/gate_self_test.js';
37
+ import { reportScanned } from './_lib/scan_scope.js';
38
+ import {
39
+ CLAIMS_PATH,
40
+ CLAIM_ID,
41
+ RECORD_PATH,
42
+ claimBlock,
43
+ claimProblems,
44
+ parseClaimFigures,
45
+ readRecord,
46
+ } from './report_skill_activation.js';
47
+
48
+ const _HERE = fileURLToPath(import.meta.url);
49
+ const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
50
+
51
+ export function check(root: string): number {
52
+ const rec = readRecord(root);
53
+ if (rec === null) {
54
+ process.stderr.write(`❌ ${RECORD_PATH} is missing — the published figure has no record behind it.\n`);
55
+ return 1;
56
+ }
57
+ const claimsFile = path.join(root, CLAIMS_PATH);
58
+ if (!fs.existsSync(claimsFile)) {
59
+ process.stderr.write(`❌ ${CLAIMS_PATH} is missing.\n`);
60
+ return 1;
61
+ }
62
+ const block = claimBlock(fs.readFileSync(claimsFile, 'utf8'), CLAIM_ID);
63
+ // Emitted before the verdict on both paths: a gate that reports what it
64
+ // inspected only when it passes leaves the coverage census blind exactly when
65
+ // it matters.
66
+ reportScanned({
67
+ gate: 'check_skill_activation_claim',
68
+ scanned: block === null ? 0 : 1,
69
+ units: 'published claim block(s)',
70
+ roots: [CLAIMS_PATH],
71
+ });
72
+ if (block === null) {
73
+ process.stderr.write(`❌ claim \`${CLAIM_ID}\` is not in ${CLAIMS_PATH}.\n`);
74
+ return 1;
75
+ }
76
+ const problems = claimProblems(rec, parseClaimFigures(block));
77
+ if (problems.length > 0) {
78
+ process.stderr.write('❌ the published census claim and its record disagree:\n');
79
+ for (const p of problems) {
80
+ process.stderr.write(` · ${p}\n`);
81
+ }
82
+ process.stderr.write(
83
+ `\nRe-take the measurement (report_skill_activation --emit) and update the claim from ${RECORD_PATH}.\n`,
84
+ );
85
+ return 1;
86
+ }
87
+ process.stdout.write(`✅ census claim matches ${RECORD_PATH} (measured ${rec.measured_at}).\n`);
88
+ return 0;
89
+ }
90
+
91
+ /**
92
+ * The gate proving it still discriminates.
93
+ *
94
+ * The third case is the one a value comparison alone would miss: a claim that
95
+ * silently stops stating a figure would otherwise pass every equality test it
96
+ * no longer participates in.
97
+ */
98
+ function selfTest(): number {
99
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'activation-claim-selftest-'));
100
+ const record = {
101
+ schema_version: 1,
102
+ measured_at: '2026-01-02',
103
+ skills_shipped: 300,
104
+ with_trigger_key: 10,
105
+ with_trigger_corpus: 100,
106
+ trigger_key_and_corpus: 5,
107
+ human_named_only: 195,
108
+ sessions: 7,
109
+ assistant_turns: 1234,
110
+ invocations: 0,
111
+ distinct_skills_invoked: 0,
112
+ stores: ['fixture'],
113
+ };
114
+ const agreeing =
115
+ '`report_skill_activation` over 7 sessions and 1,234 assistant turns records 0 Skill ' +
116
+ 'invocations and 0 of 300 distinct skills (measured 2026-01-02). 10 declare a ' +
117
+ 'machine-matchable trigger key in frontmatter, 100 carry an `evals/triggers.json` corpus, ' +
118
+ '5 do both, and the remaining 195 are reachable only by a human naming them.';
119
+ const write = (name: string, body: string): string => {
120
+ const root = path.join(tmp, name);
121
+ fs.mkdirSync(path.join(root, path.dirname(RECORD_PATH)), { recursive: true });
122
+ fs.mkdirSync(path.join(root, path.dirname(CLAIMS_PATH)), { recursive: true });
123
+ fs.writeFileSync(path.join(root, RECORD_PATH), JSON.stringify(record));
124
+ fs.writeFileSync(
125
+ path.join(root, CLAIMS_PATH),
126
+ `# Claims\n\n### claim: ${CLAIM_ID}\n- claim: ${body}\n- kind: quant\n- status: backed\n- last_verified: 2026-01-02\n\n### claim: unrelated\n- claim: x\n`,
127
+ );
128
+ return root;
129
+ };
130
+ const run = (root: string): number =>
131
+ runGateCli(REPO_ROOT, 'src/scripts/check_skill_activation_claim.ts', ['--root', root], root);
132
+ try {
133
+ return runSelfTest({
134
+ gate: 'check_skill_activation_claim',
135
+ minCases: 4,
136
+ minRejectCases: 3,
137
+ cases: [
138
+ {
139
+ name: 'a turn count that moved away from the record is rejected',
140
+ expect: 'reject',
141
+ run: () => run(write('turns', agreeing.replace('1,234 assistant', '9,999 assistant'))),
142
+ },
143
+ {
144
+ name: 'a population redrawn to enlarge the human-named remainder is rejected',
145
+ expect: 'reject',
146
+ run: () => run(write('population', agreeing.replace('remaining 195 are', 'remaining 290 are'))),
147
+ },
148
+ {
149
+ name: 'a claim that stops stating the population split is rejected',
150
+ expect: 'reject',
151
+ run: () => run(write('dropped', agreeing.slice(0, agreeing.indexOf('. 10 declare') + 1))),
152
+ },
153
+ {
154
+ name: 'a claim reproducing every recorded figure passes',
155
+ expect: 'accept',
156
+ run: () => run(write('agreeing', agreeing)),
157
+ },
158
+ ],
159
+ });
160
+ } finally {
161
+ fs.rmSync(tmp, { recursive: true, force: true });
162
+ }
163
+ }
164
+
165
+ export function main(argv: readonly string[]): number {
166
+ if (argv.includes('--help') || argv.includes('-h')) {
167
+ process.stdout.write('usage: check_skill_activation_claim [--root DIR] [--self-test]\n');
168
+ return 0;
169
+ }
170
+ if (argv.includes('--self-test')) {
171
+ return selfTest();
172
+ }
173
+ const i = argv.indexOf('--root');
174
+ return check(i === -1 ? REPO_ROOT : (argv[i + 1] ?? REPO_ROOT));
175
+ }
176
+
177
+ if (process.argv[1] !== undefined && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
178
+ process.exit(main(process.argv.slice(2)));
179
+ }
@@ -260,6 +260,17 @@ export const SUPPRESSION_INVENTORY: readonly SuppressionSpec[] = [
260
260
  // mistyped path as a fresh baseline.
261
261
  what: 'review prompts whose recorded prompt_hash does not re-derive',
262
262
  },
263
+ {
264
+ file: 'src/config/conformance-claim-baseline.json',
265
+ listKey: 'undeclared',
266
+ tier: 'string_list',
267
+ // Absent at the base ref because this change introduces it. Remove the
268
+ // flag on the next change once the introducing commit is an ancestor of
269
+ // main — the scan below fails while it is set and the file resolves,
270
+ // so the flag closes itself rather than needing to be remembered.
271
+ newInThisChange: true,
272
+ what: 'skill and rule artefacts carrying a standard-conformance claim with no enforced_by declaration',
273
+ },
263
274
  ];
264
275
 
265
276
  export interface SuppressionFinding {
@@ -150,7 +150,7 @@ function _check_one(p: string, today: PyDate): string[] {
150
150
  if (age > MAX_AGE_DAYS) {
151
151
  errors.push(
152
152
  `${rel}: \`last_eval\` ${raw as string} is ${age}d old (> ${MAX_AGE_DAYS}d) ` +
153
- '— re-run skill_trigger_eval.py and bump it',
153
+ '— re-run skill_trigger_eval and bump it',
154
154
  );
155
155
  }
156
156
  }
@@ -20,6 +20,7 @@ import { emitSqliteTwin } from './sqlite_store.js';
20
20
  import {
21
21
  type CodeEdge,
22
22
  type CodeGraph,
23
+ type EdgeConfidence,
23
24
  EXPECTED_GRAMMAR_ABI,
24
25
  EXT_LANG,
25
26
  type Lang,
@@ -110,6 +111,87 @@ interface SymbolTable {
110
111
  classByName: Map<string, string>;
111
112
  }
112
113
 
114
+ /**
115
+ * What a resolved name is allowed to BE at a given use site.
116
+ *
117
+ * The council that reviewed this repair made the same point from both seats: a
118
+ * confidence label cannot contain a false edge, only describe one, so the
119
+ * candidate set has to be filtered by capability BEFORE a confidence is
120
+ * assigned. A `const` cannot satisfy `foo()`; a `type` alias cannot satisfy
121
+ * `new T()`. Without this, adding constant/type/enum nodes in 2.1 would feed
122
+ * the very same-name table that produced the false `path` edge in the first
123
+ * place.
124
+ */
125
+ type Capability = 'callable' | 'constructible' | 'any';
126
+ const CALLABLE_KINDS: ReadonlySet<string> = new Set(['function', 'method']);
127
+ const CONSTRUCTIBLE_KINDS: ReadonlySet<string> = new Set(['class']);
128
+
129
+ function satisfies(kind: string | undefined, want: Capability): boolean {
130
+ if (kind === undefined) return true; // a file id / external ref — unknown, not disqualified
131
+ if (want === 'callable') return CALLABLE_KINDS.has(kind);
132
+ if (want === 'constructible') return CONSTRUCTIBLE_KINDS.has(kind);
133
+ return true;
134
+ }
135
+
136
+ /**
137
+ * One file's name bindings — the thing this engine did not have, and whose
138
+ * absence is the whole of defect 1.1.
139
+ *
140
+ * `locals` is every symbol DECLARED in the file. `imports` is every name the
141
+ * file BINDS from elsewhere, keyed on the local binding name, carrying whether
142
+ * the binding was resolved from a module specifier (`exact`) or merely matched
143
+ * by name (PHP `use`, which names a fully-qualified symbol and is matched on
144
+ * its base name without checking the namespace).
145
+ */
146
+ interface FileScope {
147
+ locals: Map<string, string[]>;
148
+ imports: Map<string, { target: string; exact: boolean }>;
149
+ }
150
+
151
+ const REL_EXTS = ['.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs'];
152
+
153
+ /** POSIX-only join/dirname over graph ids, which are always POSIX-relative. */
154
+ function idDirname(id: string): string {
155
+ const i = id.lastIndexOf('/');
156
+ return i === -1 ? '' : id.slice(0, i);
157
+ }
158
+ function idJoin(dir: string, rel: string): string {
159
+ const parts = (dir === '' ? [] : dir.split('/')).concat(rel.split('/'));
160
+ const out: string[] = [];
161
+ for (const p of parts) {
162
+ if (p === '' || p === '.') continue;
163
+ if (p === '..') {
164
+ // A `..` that would escape the root is KEPT, so the caller can see
165
+ // the specifier points outside the indexed tree rather than
166
+ // silently resolving to something inside it.
167
+ if (out.length && out[out.length - 1] !== '..') out.pop();
168
+ else out.push('..');
169
+ continue;
170
+ }
171
+ out.push(p);
172
+ }
173
+ return out.join('/');
174
+ }
175
+
176
+ /**
177
+ * A relative module specifier → the id of the file node it names, or `null`
178
+ * when that file is not in this graph (out of root, or an unsupported
179
+ * extension). A bare specifier (`node:path`, `js-yaml`) is external by
180
+ * definition and returns `null` too — the caller distinguishes the two by
181
+ * looking at the specifier itself.
182
+ *
183
+ * TS source imports `./types.js` and the file on disk is `types.ts`, so the
184
+ * runtime extension is stripped before the candidate list is tried.
185
+ */
186
+ function resolveSpecifier(fromFile: string, spec: string, fileIds: ReadonlySet<string>): string | null {
187
+ if (!spec.startsWith('.')) return null;
188
+ const base = idJoin(idDirname(fromFile), spec);
189
+ const stem = base.replace(/\.(js|jsx|mjs|cjs)$/, '');
190
+ const candidates = [base, ...REL_EXTS.map((e) => stem + e), ...REL_EXTS.map((e) => base + '/index' + e)];
191
+ for (const c of candidates) if (fileIds.has(c)) return c;
192
+ return null;
193
+ }
194
+
113
195
  function push(map: Map<string, string[]>, k: string, v: string): void {
114
196
  const arr = map.get(k);
115
197
  if (arr) {
@@ -146,6 +228,11 @@ export function buildGraph(files: readonly SourceFile[], extracts: readonly File
146
228
  };
147
229
  for (const n of nodes) {
148
230
  const lang = nodeLang.get(n.id) ?? null;
231
+ // `byName` stays the CALLABLE/CONSTRUCTIBLE table it always was. The
232
+ // kinds 2.1 added (constant / type / enum) are deliberately NOT put in
233
+ // it: they are reachable through a file's own scope and through
234
+ // `imports`, and letting them satisfy a bare `foo()` by name is the
235
+ // defect this roadmap removes, not a feature it adds.
149
236
  if (n.kind === 'class' || n.kind === 'interface' || n.kind === 'trait' || n.kind === 'function') {
150
237
  push(sym.byName, k(lang, n.label), n.id);
151
238
  if (n.kind !== 'function' && !sym.classByName.has(k(lang, n.label)))
@@ -184,6 +271,87 @@ export function buildGraph(files: readonly SourceFile[], extracts: readonly File
184
271
  return null;
185
272
  };
186
273
 
274
+ const nodeKind = new Map(nodes.map((n) => [n.id, n.kind as string]));
275
+ const fileIds = new Set(nodes.filter((n) => n.kind === 'file').map((n) => n.id));
276
+ const scopes = new Map<string, FileScope>();
277
+ for (const ex of extracts) {
278
+ const locals = new Map<string, string[]>();
279
+ for (const n of ex.nodes) if (n.kind !== 'file') push(locals, n.label, n.id);
280
+ const imports = new Map<string, { target: string; exact: boolean }>();
281
+ for (const r of ex.rawEdges) {
282
+ if (r.relation !== 'imports') continue;
283
+ const local = r.localName ?? r.targetName;
284
+ if (imports.has(local)) continue; // first binding wins, deterministically
285
+ const spec = r.moduleSpecifier;
286
+ if (spec === undefined) {
287
+ // PHP `use A\B\C` — a fully-qualified NAME, not a module path.
288
+ // Resolved by base name without checking the namespace, so the
289
+ // binding is a lookup, not a syntactic fact: `exact: false`.
290
+ const hit =
291
+ sym.classByName.get(k(ex.lang, r.targetName)) ??
292
+ (sym.byName.get(k(ex.lang, r.targetName)) ?? []).find((id) => id.includes('#') && !id.includes('::'));
293
+ if (hit) imports.set(local, { target: hit, exact: false });
294
+ continue;
295
+ }
296
+ const file = resolveSpecifier(ex.file, spec, fileIds);
297
+ if (file === null) {
298
+ // Bare (`node:path`, `zod`) or relative-but-out-of-root. Either
299
+ // way the module is real and NAMED, and it is not this
300
+ // repository's same-named symbol. `import * as path from
301
+ // 'node:path'` ends here, which is the whole of 1.1.
302
+ const named = r.importKind === 'named';
303
+ imports.set(local, { target: named ? `external:${spec}#${r.targetName}` : `external:${spec}`, exact: true });
304
+ continue;
305
+ }
306
+ // A namespace or default import binds the MODULE, never a
307
+ // same-named export inside it.
308
+ const member = `${file}#${r.targetName}`;
309
+ const target = r.importKind === 'named' && nodeKind.has(member) ? member : file;
310
+ imports.set(local, { target, exact: true });
311
+ }
312
+ scopes.set(ex.file, { locals, imports });
313
+ }
314
+ const emptyScope: FileScope = { locals: new Map(), imports: new Map() };
315
+
316
+ /**
317
+ * Resolve a bare name at a use site inside `file`, capability-filtered.
318
+ *
319
+ * The ladder is the language's own: a LOCAL declaration shadows an import
320
+ * (both council seats named this, and JS/TS/PHP all agree), an import
321
+ * resolved from a specifier is next, and only then the repo-wide same-name
322
+ * table — which is a guess and is labelled as one.
323
+ */
324
+ const resolveInScope = (
325
+ file: string,
326
+ name: string,
327
+ want: Capability,
328
+ lang: Lang,
329
+ scopeOnly = false,
330
+ ): { target: string; confidence: EdgeConfidence } | null => {
331
+ const scope = scopes.get(file) ?? emptyScope;
332
+ for (const id of scope.locals.get(name) ?? [])
333
+ if (satisfies(nodeKind.get(id), want)) return { target: id, confidence: 'EXTRACTED' };
334
+ const bound = scope.imports.get(name);
335
+ if (bound && satisfies(nodeKind.get(bound.target), want))
336
+ return { target: bound.target, confidence: bound.exact ? 'EXTRACTED' : 'INFERRED' };
337
+ if (scopeOnly) return null;
338
+ const wide =
339
+ want === 'constructible'
340
+ ? sym.classByName.get(k(lang, name))
341
+ : (sym.byName.get(k(lang, name)) ?? []).find(
342
+ (id) => id.includes('#') && !id.includes('::') && satisfies(nodeKind.get(id), want),
343
+ );
344
+ // Reached only by matching a name across the whole repository, with no
345
+ // binding in this file to justify it. That is not a syntactic fact and
346
+ // no longer claims to be one.
347
+ if (wide) return { target: wide, confidence: 'INFERRED' };
348
+ return null;
349
+ };
350
+
351
+ // Deduped the same way emitted edges are, so the count is comparable to
352
+ // `edges.length` rather than being a raw call-site tally that reads as a
353
+ // larger loss than the graph actually took.
354
+ const suppressedKeys = new Set<string>();
187
355
  const edges: CodeEdge[] = [];
188
356
  const seen = new Set<string>();
189
357
  const emit = (e: CodeEdge): void => {
@@ -198,8 +366,13 @@ export function buildGraph(files: readonly SourceFile[], extracts: readonly File
198
366
  for (const ex of extracts) {
199
367
  // structural inherits edges (same-language resolution)
200
368
  for (const inh of ex.inherits) {
201
- const parentId = sym.classByName.get(k(ex.lang, inh.parentName)) ?? `symbol:${inh.parentName}`;
202
- emit({ source: inh.classId, target: parentId, relation: 'inherits', confidence: 'EXTRACTED' });
369
+ const hit = resolveInScope(ex.file, inh.parentName, 'constructible', ex.lang);
370
+ emit({
371
+ source: inh.classId,
372
+ target: hit?.target ?? `symbol:${inh.parentName}`,
373
+ relation: 'inherits',
374
+ confidence: hit?.confidence ?? 'EXTRACTED',
375
+ });
203
376
  }
204
377
  for (const r of ex.rawEdges) resolveRawEdge(r, ex);
205
378
  }
@@ -216,50 +389,50 @@ export function buildGraph(files: readonly SourceFile[], extracts: readonly File
216
389
  });
217
390
  return;
218
391
  }
219
- if (r.relation === 'imports' || r.relation === 'uses') {
220
- // A class name first, which is what `imports` and the pre-existing
221
- // `uses` sites emit. Then a FREE FUNCTION, which the registry
222
- // literals added by 1.3 point at: `{ foo: handleFoo }` names a
223
- // function, not a class, so a class-only lookup left every such edge
224
- // at `symbol:<name>` — an edge that exists and resolves nowhere,
225
- // which is barely better than the missing edge it replaced.
226
- //
227
- // Same predicate as the `free` call branch below (`#` and no `::`),
228
- // so a function reference and a function call resolve identically
229
- // rather than by two rules that can drift.
230
- const resolved =
231
- sym.classByName.get(k(ex.lang, r.targetName)) ??
232
- (sym.byName.get(k(ex.lang, r.targetName)) ?? []).find(
233
- (id) => id.includes('#') && !id.includes('::'),
234
- );
392
+ if (r.relation === 'imports') {
393
+ // The import's own binding, computed once per file above. A
394
+ // specifier-resolved binding is a syntactic fact — the file says
395
+ // where the name comes from — and a PHP `use` matched by base name
396
+ // is not, and says so.
397
+ const bound = (scopes.get(ex.file) ?? emptyScope).imports.get(r.localName ?? r.targetName);
235
398
  emit({
236
399
  source: r.sourceId,
237
- target: resolved ?? `symbol:${r.targetName}`,
238
- relation: r.relation,
239
- confidence: 'EXTRACTED',
400
+ target: bound?.target ?? `symbol:${r.targetName}`,
401
+ relation: 'imports',
402
+ confidence: bound ? (bound.exact ? 'EXTRACTED' : 'INFERRED') : 'EXTRACTED',
240
403
  });
241
404
  return;
242
405
  }
243
- // relation === 'calls'
244
- if (r.callStyle === 'new') {
245
- const resolved = sym.classByName.get(k(ex.lang, r.targetName));
406
+ if (r.relation === 'uses') {
407
+ // A registry literal (`{ foo: handleFoo }`) or an object shorthand
408
+ // (`{ id, label }`) names something bound IN THIS FILE. It is
409
+ // resolved through the file's scope and NOWHERE else: a shorthand
410
+ // property almost always names a local variable, and matching it
411
+ // against a same-named declaration elsewhere in the repository is
412
+ // the defect this roadmap removes, applied to a second relation.
413
+ const hit = resolveInScope(ex.file, r.targetName, 'any', ex.lang, true);
246
414
  emit({
247
415
  source: r.sourceId,
248
- target: resolved ?? `symbol:${r.targetName}`,
249
- relation: 'calls',
250
- confidence: 'EXTRACTED',
416
+ target: hit?.target ?? `symbol:${r.targetName}`,
417
+ relation: 'uses',
418
+ confidence: hit?.confidence ?? 'EXTRACTED',
251
419
  });
252
420
  return;
253
421
  }
254
- if (r.callStyle === 'free') {
255
- const resolved = (sym.byName.get(k(ex.lang, r.targetName)) ?? []).find(
256
- (id) => id.includes('#') && !id.includes('::'),
257
- );
422
+ // relation === 'calls'
423
+ if (r.callStyle === 'new' || r.callStyle === 'free') {
424
+ const want: Capability = r.callStyle === 'new' ? 'constructible' : 'callable';
425
+ const hit = resolveInScope(ex.file, r.targetName, want, ex.lang);
258
426
  emit({
259
427
  source: r.sourceId,
260
- target: resolved ?? `symbol:${r.targetName}`,
428
+ target: hit?.target ?? `symbol:${r.targetName}`,
261
429
  relation: 'calls',
262
- confidence: 'EXTRACTED',
430
+ // An UNRESOLVED name stays EXTRACTED: `symbol:strlen` claims
431
+ // nothing about a node, it records that a call to a name this
432
+ // repository does not declare happened here. What is no longer
433
+ // EXTRACTED is a claim about a SPECIFIC in-repo node reached by
434
+ // name alone.
435
+ confidence: hit?.confidence ?? 'EXTRACTED',
263
436
  });
264
437
  return;
265
438
  }
@@ -287,6 +460,27 @@ export function buildGraph(files: readonly SourceFile[], extracts: readonly File
287
460
  }
288
461
  // scoped (Class::m unresolved / facade) or dynamic ($obj->m) → AMBIGUOUS
289
462
  const cands = methodCandidates(r.targetName, ex.lang);
463
+ if (r.callStyle === 'dynamic' && cands.length === 0) {
464
+ // `x.push()`, `m.get()`, `s.join()` — a dynamic member call whose
465
+ // method name matches NO method declared in this repository. The
466
+ // only target available is `symbol:push`, which is not a node, so
467
+ // no query verb can reach it: `affected` and `path` resolve seeds
468
+ // against real nodes and a pseudo-node is never one. It was 257 of
469
+ // 660 edges on this engine's own source — 39 % of the graph
470
+ // asserting "a method named push was called somewhere".
471
+ //
472
+ // NOT suppressed: a `scoped` call (`Cache::get()`) and a
473
+ // `hierarchy-unresolved` `$this->missing()` both name a real
474
+ // unresolved intent inside a known receiver, which a later
475
+ // resolution pass could settle. This drops only the class where
476
+ // there is nothing to settle.
477
+ //
478
+ // The count is published in `suppressed_edge_counts` — an engine
479
+ // that silently deletes its worst edges reports a flattering
480
+ // confidence ratio and no evidence.
481
+ suppressedKeys.add(`${r.sourceId}\0calls\0symbol:${r.targetName}`);
482
+ return;
483
+ }
290
484
  emit({
291
485
  source: r.sourceId,
292
486
  target: ambiguousTarget(cands, r.targetName),
@@ -310,6 +504,7 @@ export function buildGraph(files: readonly SourceFile[], extracts: readonly File
310
504
 
311
505
  const counts = { EXTRACTED: 0, INFERRED: 0, AMBIGUOUS: 0 };
312
506
  for (const e of edges) counts[e.confidence] += 1;
507
+ const suppressed = { dynamic_no_candidate: suppressedKeys.size };
313
508
 
314
509
  const languages = [...new Set(extracts.map((e) => e.lang))].sort() as Lang[];
315
510
 
@@ -328,6 +523,7 @@ export function buildGraph(files: readonly SourceFile[], extracts: readonly File
328
523
  languages,
329
524
  grammar_abi: EXPECTED_GRAMMAR_ABI,
330
525
  edge_confidence_counts: counts,
526
+ suppressed_edge_counts: suppressed,
331
527
  nodes,
332
528
  edges,
333
529
  };
@@ -370,6 +566,7 @@ export function serializeGraph(g: CodeGraph): string {
370
566
  'languages',
371
567
  'grammar_abi',
372
568
  'edge_confidence_counts',
569
+ 'suppressed_edge_counts',
373
570
  'nodes',
374
571
  'edges',
375
572
  ];