@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
package/src/scripts/ci_settle.ts
CHANGED
|
@@ -18,10 +18,20 @@
|
|
|
18
18
|
* every agent's head: `not settled` and `settled` are different from `could not
|
|
19
19
|
* read`, and only the first two may end a wait.
|
|
20
20
|
*
|
|
21
|
-
*
|
|
21
|
+
* A verdict also has to be ABOUT SOMETHING, which is the second half of the
|
|
22
|
+
* same rule and was missing until 2026-09-08: the rollup answers about the head
|
|
23
|
+
* the PR *record* carries, and that head stops tracking the branch when the PR
|
|
24
|
+
* is merged or when the record lags a push. Both then look like a healthy
|
|
25
|
+
* green. {@link classifyTarget} refuses those, before the wait and again at
|
|
26
|
+
* verdict time. Measured cost of not having it: a `SETTLED GREEN — 40 check(s)`
|
|
27
|
+
* for a head with literally zero check runs.
|
|
28
|
+
*
|
|
29
|
+
* Exit codes: 0 = settled green, and the head it is about is named ·
|
|
22
30
|
* 1 = settled, at least one check failed ·
|
|
23
|
-
* 2 = did not settle within the budget,
|
|
24
|
-
*
|
|
31
|
+
* 2 = did not settle within the budget, the API could not be read,
|
|
32
|
+
* or the target is not a PR a verdict can be about (merged,
|
|
33
|
+
* closed, or head-diverged) — NEVER a verdict, and the reason
|
|
34
|
+
* is printed.
|
|
25
35
|
*
|
|
26
36
|
* Usage: ./scripts-run src/scripts/ci_settle <pr> [--timeout-min N] [--interval-sec N]
|
|
27
37
|
*
|
|
@@ -57,6 +67,121 @@ export type PollState =
|
|
|
57
67
|
| { kind: 'pending'; total: number; done: number }
|
|
58
68
|
| { kind: 'settled'; failing: string[]; total: number };
|
|
59
69
|
|
|
70
|
+
/**
|
|
71
|
+
* What `git ls-remote` said about the PR branch's tip.
|
|
72
|
+
*
|
|
73
|
+
* `absent` and `unreadable` are different answers and must not collapse: a
|
|
74
|
+
* cross-repo fork PR legitimately has no ref under this `origin`, while a
|
|
75
|
+
* transport failure means nobody knows. Collapsing them either disables the
|
|
76
|
+
* divergence check for forks silently, or blocks every fork PR forever.
|
|
77
|
+
*/
|
|
78
|
+
export type RemoteTip =
|
|
79
|
+
| { kind: 'found'; sha: string }
|
|
80
|
+
| { kind: 'absent' }
|
|
81
|
+
| { kind: 'unreadable'; reason: string };
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Whether the PR this wait is about still decides anything.
|
|
85
|
+
*
|
|
86
|
+
* `refuse` is NOT a verdict — it exits 2 like every other non-verdict, because
|
|
87
|
+
* the wait produced no readable statement about the head that matters.
|
|
88
|
+
*/
|
|
89
|
+
export type TargetState =
|
|
90
|
+
| { kind: 'open'; head: string; ref: string; divergenceChecked: boolean }
|
|
91
|
+
| { kind: 'unreadable'; reason: string }
|
|
92
|
+
| { kind: 'refuse'; reason: string };
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Decide whether a green from {@link classifyPoll} may be reported at all.
|
|
96
|
+
*
|
|
97
|
+
* WHY THIS EXISTS, measured 2026-09-08 on PR #1949 and paid for twice in one
|
|
98
|
+
* session. `gh pr view --json statusCheckRollup` answers about the head the PR
|
|
99
|
+
* *record* carries, and that head stops tracking the branch in two situations
|
|
100
|
+
* this script previously could not tell apart from a healthy wait:
|
|
101
|
+
*
|
|
102
|
+
* 1. **The PR is already merged or closed.** A push to its branch fires no
|
|
103
|
+
* `pull_request` event, so no checks are created for the new commits and the
|
|
104
|
+
* rollup keeps returning the merge-time head's checks. The script reported
|
|
105
|
+
* `SETTLED GREEN — 40 check(s)` for a head that had stopped mattering, and
|
|
106
|
+
* the only thing that exposed it was a hand-written
|
|
107
|
+
* `/commits/<sha>/check-runs` query answering `0`. Reported at face value
|
|
108
|
+
* that green authorises a merge of content nobody checked.
|
|
109
|
+
* 2. **The PR record lags the branch ref.** Same shape, different cause
|
|
110
|
+
* (recorded on PR #710): `git ls-remote` shows the new SHA while the record
|
|
111
|
+
* keeps the old one, and merging then lands the PR WITHOUT the newer
|
|
112
|
+
* commits — silently, which is what makes it worse than a red.
|
|
113
|
+
*
|
|
114
|
+
* Both are answered by the same two facts, which is why one function decides
|
|
115
|
+
* them: the PR's `state`, and whether its head equals the branch tip. Pure, so
|
|
116
|
+
* the failure is testable without a network — feed it a merged PR and it must
|
|
117
|
+
* answer `refuse`, never `open`.
|
|
118
|
+
*/
|
|
119
|
+
export function classifyTarget(
|
|
120
|
+
stdout: string,
|
|
121
|
+
stderr: string,
|
|
122
|
+
status: number | null,
|
|
123
|
+
tip: RemoteTip,
|
|
124
|
+
): TargetState {
|
|
125
|
+
const combined = `${stdout}\n${stderr}`.toLowerCase();
|
|
126
|
+
for (const marker of _UNREADABLE_MARKERS) {
|
|
127
|
+
if (combined.includes(marker.toLowerCase())) {
|
|
128
|
+
return { kind: 'unreadable', reason: marker };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (status !== 0) {
|
|
132
|
+
return { kind: 'unreadable', reason: `gh exited ${String(status)}` };
|
|
133
|
+
}
|
|
134
|
+
let state: string;
|
|
135
|
+
let head: string;
|
|
136
|
+
let ref: string;
|
|
137
|
+
try {
|
|
138
|
+
const parsed = JSON.parse(stdout) as {
|
|
139
|
+
state?: unknown;
|
|
140
|
+
headRefOid?: unknown;
|
|
141
|
+
headRefName?: unknown;
|
|
142
|
+
};
|
|
143
|
+
if (
|
|
144
|
+
typeof parsed.state !== 'string' ||
|
|
145
|
+
typeof parsed.headRefOid !== 'string' ||
|
|
146
|
+
typeof parsed.headRefName !== 'string'
|
|
147
|
+
) {
|
|
148
|
+
return { kind: 'unreadable', reason: 'no state / headRefOid / headRefName in response' };
|
|
149
|
+
}
|
|
150
|
+
state = parsed.state.toUpperCase();
|
|
151
|
+
head = parsed.headRefOid;
|
|
152
|
+
ref = parsed.headRefName;
|
|
153
|
+
} catch {
|
|
154
|
+
return { kind: 'unreadable', reason: 'unparseable JSON' };
|
|
155
|
+
}
|
|
156
|
+
if (state !== 'OPEN') {
|
|
157
|
+
return {
|
|
158
|
+
kind: 'refuse',
|
|
159
|
+
reason:
|
|
160
|
+
`the PR is ${state}, not OPEN. Its rollup describes head ${head.slice(0, 9)} as of ` +
|
|
161
|
+
'that transition; pushes after it create no checks, so a green here would be about ' +
|
|
162
|
+
'a head that no longer decides anything',
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
if (tip.kind === 'unreadable') {
|
|
166
|
+
return { kind: 'unreadable', reason: `branch tip: ${tip.reason}` };
|
|
167
|
+
}
|
|
168
|
+
if (tip.kind === 'absent') {
|
|
169
|
+
// A fork PR's ref is not under this origin. Say the check did not apply
|
|
170
|
+
// rather than pretending it passed.
|
|
171
|
+
return { kind: 'open', head, ref, divergenceChecked: false };
|
|
172
|
+
}
|
|
173
|
+
if (tip.sha !== head) {
|
|
174
|
+
return {
|
|
175
|
+
kind: 'refuse',
|
|
176
|
+
reason:
|
|
177
|
+
`the PR record is at head ${head.slice(0, 9)} but origin/${ref} is at ` +
|
|
178
|
+
`${tip.sha.slice(0, 9)}. Merging in this state lands the PR WITHOUT the newer ` +
|
|
179
|
+
'commits (recorded on PR #710), and any verdict here is about the wrong head',
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
return { kind: 'open', head, ref, divergenceChecked: true };
|
|
183
|
+
}
|
|
184
|
+
|
|
60
185
|
/** Markers that mean the API did not answer — not that nothing is pending. */
|
|
61
186
|
const _UNREADABLE_MARKERS = [
|
|
62
187
|
'error connecting to',
|
|
@@ -126,6 +251,41 @@ function poll(pr: string): PollState {
|
|
|
126
251
|
return classifyPoll(r.stdout ?? '', r.stderr ?? '', r.status);
|
|
127
252
|
}
|
|
128
253
|
|
|
254
|
+
/**
|
|
255
|
+
* Read the branch tip from the remote, not from a local ref.
|
|
256
|
+
*
|
|
257
|
+
* A local `origin/<ref>` is only as fresh as the last fetch, and the divergence
|
|
258
|
+
* this guards against is created by somebody else's push — so a stale local ref
|
|
259
|
+
* would report agreement exactly when it matters most.
|
|
260
|
+
*/
|
|
261
|
+
function readTip(ref: string): RemoteTip {
|
|
262
|
+
const r = spawnSync('git', ['ls-remote', 'origin', `refs/heads/${ref}`], {
|
|
263
|
+
encoding: 'utf-8',
|
|
264
|
+
timeout: 60_000,
|
|
265
|
+
});
|
|
266
|
+
if (r.status !== 0) {
|
|
267
|
+
const why = (r.stderr ?? '').trim().split('\n')[0] ?? `git exited ${String(r.status)}`;
|
|
268
|
+
return { kind: 'unreadable', reason: why };
|
|
269
|
+
}
|
|
270
|
+
const line = (r.stdout ?? '').trim();
|
|
271
|
+
if (line === '') return { kind: 'absent' };
|
|
272
|
+
const sha = line.split(/\s+/)[0] ?? '';
|
|
273
|
+
return sha === '' ? { kind: 'unreadable', reason: 'empty sha in ls-remote output' } : { kind: 'found', sha };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function readTarget(pr: string): TargetState {
|
|
277
|
+
const r = spawnSync('gh', ['pr', 'view', pr, '--json', 'state,headRefOid,headRefName'], {
|
|
278
|
+
encoding: 'utf-8',
|
|
279
|
+
timeout: 60_000,
|
|
280
|
+
});
|
|
281
|
+
// The ref name comes from the same read, so the tip lookup needs it first;
|
|
282
|
+
// classifyTarget is called twice rather than plumbing a callback through a
|
|
283
|
+
// pure function.
|
|
284
|
+
const shape = classifyTarget(r.stdout ?? '', r.stderr ?? '', r.status, { kind: 'absent' });
|
|
285
|
+
if (shape.kind !== 'open') return shape;
|
|
286
|
+
return classifyTarget(r.stdout ?? '', r.stderr ?? '', r.status, readTip(shape.ref));
|
|
287
|
+
}
|
|
288
|
+
|
|
129
289
|
function sleepSync(seconds: number): void {
|
|
130
290
|
// Deliberately synchronous: this is a CLI whose whole job is to block.
|
|
131
291
|
spawnSync(process.execPath, ['-e', `setTimeout(()=>{}, ${String(seconds * 1000)})`], {
|
|
@@ -164,18 +324,58 @@ export function main(argv: readonly string[]): number {
|
|
|
164
324
|
}
|
|
165
325
|
const intervalSec = num('--interval-sec', 60);
|
|
166
326
|
|
|
327
|
+
// Up front, because waiting nine minutes to learn the PR was merged before
|
|
328
|
+
// the wait began is the expensive way to find out.
|
|
329
|
+
const before = readTarget(pr);
|
|
330
|
+
if (before.kind === 'refuse') {
|
|
331
|
+
process.stdout.write(`ci_settle: REFUSING TO WAIT — ${before.reason}. No verdict is claimed.\n`);
|
|
332
|
+
return 2;
|
|
333
|
+
}
|
|
334
|
+
if (before.kind === 'unreadable') {
|
|
335
|
+
process.stdout.write(
|
|
336
|
+
`ci_settle: could not read the PR's target state (${before.reason}) — NOT a verdict, waiting anyway; ` +
|
|
337
|
+
'the pre-verdict re-check below still applies.\n',
|
|
338
|
+
);
|
|
339
|
+
} else if (!before.divergenceChecked) {
|
|
340
|
+
process.stdout.write(
|
|
341
|
+
`ci_settle: origin carries no refs/heads/${before.ref} (a fork PR) — the head-divergence check does not apply here.\n`,
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
|
|
167
345
|
const deadline = Date.now() + timeoutMin * 60_000;
|
|
168
346
|
let unreadableStreak = 0;
|
|
169
347
|
|
|
170
348
|
for (;;) {
|
|
171
349
|
const state = poll(pr);
|
|
172
350
|
if (state.kind === 'settled') {
|
|
351
|
+
// The rollup is about the record's head. Re-read that head at
|
|
352
|
+
// VERDICT time, not only at start: a push or a merge during the
|
|
353
|
+
// wait makes the rollup describe something the caller is not about
|
|
354
|
+
// to act on, and this is the last moment a wrong verdict is still
|
|
355
|
+
// preventable.
|
|
356
|
+
const at = readTarget(pr);
|
|
357
|
+
if (at.kind === 'refuse') {
|
|
358
|
+
process.stdout.write(
|
|
359
|
+
`ci_settle: WITHHOLDING A VERDICT — ${at.reason}. The checks that settled are not about ` +
|
|
360
|
+
'the head that matters now.\n',
|
|
361
|
+
);
|
|
362
|
+
return 2;
|
|
363
|
+
}
|
|
364
|
+
if (at.kind === 'unreadable') {
|
|
365
|
+
process.stdout.write(
|
|
366
|
+
`ci_settle: WITHHOLDING A VERDICT — could not confirm the target head (${at.reason}). ` +
|
|
367
|
+
'A verdict needs to name the head it is about.\n',
|
|
368
|
+
);
|
|
369
|
+
return 2;
|
|
370
|
+
}
|
|
173
371
|
if (state.failing.length === 0) {
|
|
174
|
-
process.stdout.write(
|
|
372
|
+
process.stdout.write(
|
|
373
|
+
`ci_settle: SETTLED GREEN — ${String(state.total)} check(s) on ${at.head.slice(0, 9)}\n`,
|
|
374
|
+
);
|
|
175
375
|
return 0;
|
|
176
376
|
}
|
|
177
377
|
process.stdout.write(
|
|
178
|
-
`ci_settle: SETTLED RED — ${String(state.failing.length)} of ${String(state.total)} failing:\n` +
|
|
378
|
+
`ci_settle: SETTLED RED — ${String(state.failing.length)} of ${String(state.total)} failing on ${at.head.slice(0, 9)}:\n` +
|
|
179
379
|
state.failing.map((f) => ` ${f}\n`).join(''),
|
|
180
380
|
);
|
|
181
381
|
return 1;
|
|
@@ -16,6 +16,17 @@ import * as path from 'node:path';
|
|
|
16
16
|
|
|
17
17
|
import { write_atomic } from '../_lib/fs_atomic.js';
|
|
18
18
|
import { extractFile, type FileExtract, type RawEdge } from './extract.js';
|
|
19
|
+
import {
|
|
20
|
+
ALIAS_VIA,
|
|
21
|
+
aliasRulesFrom,
|
|
22
|
+
EMPTY_RESOLUTION_CONFIG,
|
|
23
|
+
parseJsonc,
|
|
24
|
+
PSR4_VIA,
|
|
25
|
+
psr4RulesFrom,
|
|
26
|
+
type ResolutionConfig,
|
|
27
|
+
resolveAlias,
|
|
28
|
+
resolvePsr4,
|
|
29
|
+
} from './resolution_tiers.js';
|
|
19
30
|
import { emitSqliteTwin } from './sqlite_store.js';
|
|
20
31
|
import {
|
|
21
32
|
type CodeEdge,
|
|
@@ -23,7 +34,9 @@ import {
|
|
|
23
34
|
type EdgeConfidence,
|
|
24
35
|
EXPECTED_GRAMMAR_ABI,
|
|
25
36
|
EXT_LANG,
|
|
37
|
+
isStatedResolution,
|
|
26
38
|
type Lang,
|
|
39
|
+
type ResolvedVia,
|
|
27
40
|
SCHEMA_VERSION,
|
|
28
41
|
} from './types.js';
|
|
29
42
|
|
|
@@ -145,7 +158,9 @@ function satisfies(kind: string | undefined, want: Capability): boolean {
|
|
|
145
158
|
*/
|
|
146
159
|
interface FileScope {
|
|
147
160
|
locals: Map<string, string[]>;
|
|
148
|
-
|
|
161
|
+
/** `via` records WHICH tier produced the binding, so an edge built from it
|
|
162
|
+
* reports the mechanism instead of assuming `import-specifier`. */
|
|
163
|
+
imports: Map<string, { target: string; exact: boolean; via: ResolvedVia }>;
|
|
149
164
|
}
|
|
150
165
|
|
|
151
166
|
const REL_EXTS = ['.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs'];
|
|
@@ -199,11 +214,40 @@ function push(map: Map<string, string[]>, k: string, v: string): void {
|
|
|
199
214
|
} else map.set(k, [v]);
|
|
200
215
|
}
|
|
201
216
|
|
|
217
|
+
/**
|
|
218
|
+
* Read the project's declared resolution tiers from `root` (2.3).
|
|
219
|
+
*
|
|
220
|
+
* IO lives here rather than in `buildGraph`, which stays pure so identical
|
|
221
|
+
* source plus identical config yields identical bytes. A missing or
|
|
222
|
+
* unparseable config is not an error — it degrades to no tiers, which is the
|
|
223
|
+
* behaviour every build had before this existed.
|
|
224
|
+
*
|
|
225
|
+
* `tsconfig.json` is read as JSONC: it is JSONC by convention and this
|
|
226
|
+
* repository's own carries `//` comments, so `JSON.parse` on it throws.
|
|
227
|
+
*/
|
|
228
|
+
export function readResolutionConfig(root: string): ResolutionConfig {
|
|
229
|
+
const read = (name: string): unknown | null => {
|
|
230
|
+
try {
|
|
231
|
+
return parseJsonc(fs.readFileSync(path.join(root, name), 'utf-8'));
|
|
232
|
+
} catch {
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
return {
|
|
237
|
+
aliases: aliasRulesFrom(read('tsconfig.json')),
|
|
238
|
+
psr4: psr4RulesFrom(read('composer.json')),
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
202
242
|
/**
|
|
203
243
|
* Pure, deterministic graph build over an in-memory file set. Exposed for
|
|
204
244
|
* tests (no IO). `buildFromRepo` is the IO wrapper.
|
|
205
245
|
*/
|
|
206
|
-
export function buildGraph(
|
|
246
|
+
export function buildGraph(
|
|
247
|
+
files: readonly SourceFile[],
|
|
248
|
+
extracts: readonly FileExtract[],
|
|
249
|
+
resolution: ResolutionConfig = EMPTY_RESOLUTION_CONFIG,
|
|
250
|
+
): CodeGraph {
|
|
207
251
|
const nodes = extracts.flatMap((e) => e.nodes);
|
|
208
252
|
const nodeIds = new Set(nodes.map((n) => n.id));
|
|
209
253
|
|
|
@@ -277,7 +321,7 @@ export function buildGraph(files: readonly SourceFile[], extracts: readonly File
|
|
|
277
321
|
for (const ex of extracts) {
|
|
278
322
|
const locals = new Map<string, string[]>();
|
|
279
323
|
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 }>();
|
|
324
|
+
const imports = new Map<string, { target: string; exact: boolean; via: ResolvedVia }>();
|
|
281
325
|
for (const r of ex.rawEdges) {
|
|
282
326
|
if (r.relation !== 'imports') continue;
|
|
283
327
|
const local = r.localName ?? r.targetName;
|
|
@@ -285,29 +329,58 @@ export function buildGraph(files: readonly SourceFile[], extracts: readonly File
|
|
|
285
329
|
const spec = r.moduleSpecifier;
|
|
286
330
|
if (spec === undefined) {
|
|
287
331
|
// PHP `use A\B\C` — a fully-qualified NAME, not a module path.
|
|
288
|
-
//
|
|
289
|
-
//
|
|
332
|
+
//
|
|
333
|
+
// TIER 1 (2.3): composer PSR-4 maps the NAMESPACE to a
|
|
334
|
+
// directory, so the class file is named outright. That is a
|
|
335
|
+
// declared mapping, hence `exact: true` — and it is the only
|
|
336
|
+
// rung that can tell two same-named classes in different
|
|
337
|
+
// namespaces apart.
|
|
338
|
+
const fq = r.fqName ?? r.targetName;
|
|
339
|
+
const psr4File = resolvePsr4(fq, resolution, fileIds);
|
|
340
|
+
if (psr4File !== null) {
|
|
341
|
+
const member = `${psr4File}#${r.targetName}`;
|
|
342
|
+
imports.set(local, {
|
|
343
|
+
target: nodeKind.has(member) ? member : psr4File,
|
|
344
|
+
exact: true,
|
|
345
|
+
via: PSR4_VIA,
|
|
346
|
+
});
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
// TIER 2: base name, namespace discarded. A lookup, not a
|
|
350
|
+
// syntactic fact: `exact: false`.
|
|
290
351
|
const hit =
|
|
291
352
|
sym.classByName.get(k(ex.lang, r.targetName)) ??
|
|
292
353
|
(sym.byName.get(k(ex.lang, r.targetName)) ?? []).find((id) => id.includes('#') && !id.includes('::'));
|
|
293
|
-
if (hit) imports.set(local, { target: hit, exact: false });
|
|
354
|
+
if (hit) imports.set(local, { target: hit, exact: false, via: 'name-lookup' });
|
|
294
355
|
continue;
|
|
295
356
|
}
|
|
296
|
-
|
|
357
|
+
// TIER 0: a relative specifier — the file says where it points.
|
|
358
|
+
// TIER 1 (2.3): tsconfig `paths`. `@shared/x` IS `src/shared/x`
|
|
359
|
+
// because the project declares it, so this is a fact and not a
|
|
360
|
+
// guess. Without it the specifier is non-relative, `resolveSpecifier`
|
|
361
|
+
// returns null, and the import binds to `external:@shared/x` — a
|
|
362
|
+
// real module, correctly named, and the wrong one.
|
|
363
|
+
const relFile = resolveSpecifier(ex.file, spec, fileIds);
|
|
364
|
+
const aliasFile = relFile === null ? resolveAlias(spec, resolution, fileIds) : null;
|
|
365
|
+
const file = relFile ?? aliasFile;
|
|
297
366
|
if (file === null) {
|
|
298
367
|
// Bare (`node:path`, `zod`) or relative-but-out-of-root. Either
|
|
299
368
|
// way the module is real and NAMED, and it is not this
|
|
300
369
|
// repository's same-named symbol. `import * as path from
|
|
301
370
|
// 'node:path'` ends here, which is the whole of 1.1.
|
|
302
371
|
const named = r.importKind === 'named';
|
|
303
|
-
imports.set(local, {
|
|
372
|
+
imports.set(local, {
|
|
373
|
+
target: named ? `external:${spec}#${r.targetName}` : `external:${spec}`,
|
|
374
|
+
exact: true,
|
|
375
|
+
via: 'import-specifier',
|
|
376
|
+
});
|
|
304
377
|
continue;
|
|
305
378
|
}
|
|
306
379
|
// A namespace or default import binds the MODULE, never a
|
|
307
380
|
// same-named export inside it.
|
|
308
381
|
const member = `${file}#${r.targetName}`;
|
|
309
382
|
const target = r.importKind === 'named' && nodeKind.has(member) ? member : file;
|
|
310
|
-
imports.set(local, { target, exact: true });
|
|
383
|
+
imports.set(local, { target, exact: true, via: aliasFile === null ? 'import-specifier' : ALIAS_VIA });
|
|
311
384
|
}
|
|
312
385
|
scopes.set(ex.file, { locals, imports });
|
|
313
386
|
}
|
|
@@ -327,13 +400,23 @@ export function buildGraph(files: readonly SourceFile[], extracts: readonly File
|
|
|
327
400
|
want: Capability,
|
|
328
401
|
lang: Lang,
|
|
329
402
|
scopeOnly = false,
|
|
330
|
-
): { target: string; confidence: EdgeConfidence } | null => {
|
|
403
|
+
): { target: string; confidence: EdgeConfidence; via: ResolvedVia } | null => {
|
|
331
404
|
const scope = scopes.get(file) ?? emptyScope;
|
|
405
|
+
// The ladder's rungs ARE the mechanism taxonomy, so `via` is read off
|
|
406
|
+
// the branch that succeeded rather than inferred afterwards.
|
|
332
407
|
for (const id of scope.locals.get(name) ?? [])
|
|
333
|
-
if (satisfies(nodeKind.get(id), want))
|
|
408
|
+
if (satisfies(nodeKind.get(id), want))
|
|
409
|
+
return { target: id, confidence: 'EXTRACTED', via: 'same-file' };
|
|
334
410
|
const bound = scope.imports.get(name);
|
|
335
411
|
if (bound && satisfies(nodeKind.get(bound.target), want))
|
|
336
|
-
return {
|
|
412
|
+
return {
|
|
413
|
+
target: bound.target,
|
|
414
|
+
confidence: bound.exact ? 'EXTRACTED' : 'INFERRED',
|
|
415
|
+
// The binding knows how it was made — a tsconfig alias and a
|
|
416
|
+
// PSR-4 namespace are not import specifiers and must not be
|
|
417
|
+
// reported as ones.
|
|
418
|
+
via: bound.via,
|
|
419
|
+
};
|
|
337
420
|
if (scopeOnly) return null;
|
|
338
421
|
const wide =
|
|
339
422
|
want === 'constructible'
|
|
@@ -344,10 +427,19 @@ export function buildGraph(files: readonly SourceFile[], extracts: readonly File
|
|
|
344
427
|
// Reached only by matching a name across the whole repository, with no
|
|
345
428
|
// binding in this file to justify it. That is not a syntactic fact and
|
|
346
429
|
// no longer claims to be one.
|
|
347
|
-
if (wide) return { target: wide, confidence: 'INFERRED' };
|
|
430
|
+
if (wide) return { target: wide, confidence: 'INFERRED', via: 'name-lookup' };
|
|
348
431
|
return null;
|
|
349
432
|
};
|
|
350
433
|
|
|
434
|
+
/**
|
|
435
|
+
* The mechanism for an edge whose target did not resolve.
|
|
436
|
+
*
|
|
437
|
+
* A `symbol:` pseudo-target is the residue of a name lookup that found
|
|
438
|
+
* nothing, so `name-lookup` is what actually happened. `confidence` already
|
|
439
|
+
* carries whether it resolved; this field carries what was tried.
|
|
440
|
+
*/
|
|
441
|
+
const UNRESOLVED_VIA: ResolvedVia = 'name-lookup';
|
|
442
|
+
|
|
351
443
|
// Deduped the same way emitted edges are, so the count is comparable to
|
|
352
444
|
// `edges.length` rather than being a raw call-site tally that reads as a
|
|
353
445
|
// larger loss than the graph actually took.
|
|
@@ -372,6 +464,8 @@ export function buildGraph(files: readonly SourceFile[], extracts: readonly File
|
|
|
372
464
|
target: hit?.target ?? `symbol:${inh.parentName}`,
|
|
373
465
|
relation: 'inherits',
|
|
374
466
|
confidence: hit?.confidence ?? 'EXTRACTED',
|
|
467
|
+
resolved_via: hit?.via ?? UNRESOLVED_VIA,
|
|
468
|
+
provider: 'native',
|
|
375
469
|
});
|
|
376
470
|
}
|
|
377
471
|
for (const r of ex.rawEdges) resolveRawEdge(r, ex);
|
|
@@ -386,6 +480,11 @@ export function buildGraph(files: readonly SourceFile[], extracts: readonly File
|
|
|
386
480
|
target: nodeIds.has(target) ? target : `symbol:${r.targetName}`,
|
|
387
481
|
relation: 'member',
|
|
388
482
|
confidence: 'EXTRACTED',
|
|
483
|
+
// A member id is DERIVED from its own source node's id, so the
|
|
484
|
+
// declaration is in the same file by construction — no lookup
|
|
485
|
+
// of any kind happens here.
|
|
486
|
+
resolved_via: nodeIds.has(target) ? 'same-file' : UNRESOLVED_VIA,
|
|
487
|
+
provider: 'native',
|
|
389
488
|
});
|
|
390
489
|
return;
|
|
391
490
|
}
|
|
@@ -400,6 +499,8 @@ export function buildGraph(files: readonly SourceFile[], extracts: readonly File
|
|
|
400
499
|
target: bound?.target ?? `symbol:${r.targetName}`,
|
|
401
500
|
relation: 'imports',
|
|
402
501
|
confidence: bound ? (bound.exact ? 'EXTRACTED' : 'INFERRED') : 'EXTRACTED',
|
|
502
|
+
resolved_via: bound ? bound.via : UNRESOLVED_VIA,
|
|
503
|
+
provider: 'native',
|
|
403
504
|
});
|
|
404
505
|
return;
|
|
405
506
|
}
|
|
@@ -416,6 +517,8 @@ export function buildGraph(files: readonly SourceFile[], extracts: readonly File
|
|
|
416
517
|
target: hit?.target ?? `symbol:${r.targetName}`,
|
|
417
518
|
relation: 'uses',
|
|
418
519
|
confidence: hit?.confidence ?? 'EXTRACTED',
|
|
520
|
+
resolved_via: hit?.via ?? UNRESOLVED_VIA,
|
|
521
|
+
provider: 'native',
|
|
419
522
|
});
|
|
420
523
|
return;
|
|
421
524
|
}
|
|
@@ -433,13 +536,25 @@ export function buildGraph(files: readonly SourceFile[], extracts: readonly File
|
|
|
433
536
|
// EXTRACTED is a claim about a SPECIFIC in-repo node reached by
|
|
434
537
|
// name alone.
|
|
435
538
|
confidence: hit?.confidence ?? 'EXTRACTED',
|
|
539
|
+
resolved_via: hit?.via ?? UNRESOLVED_VIA,
|
|
540
|
+
provider: 'native',
|
|
436
541
|
});
|
|
437
542
|
return;
|
|
438
543
|
}
|
|
439
544
|
if (r.callStyle === 'this' || r.callStyle === 'self' || r.callStyle === 'static' || r.callStyle === 'parent') {
|
|
440
545
|
const hit = r.enclosingClassId ? resolveHierMethod(r.enclosingClassId, r.targetName) : null;
|
|
441
546
|
if (hit) {
|
|
442
|
-
emit({
|
|
547
|
+
emit({
|
|
548
|
+
source: r.sourceId,
|
|
549
|
+
target: hit,
|
|
550
|
+
relation: 'calls',
|
|
551
|
+
confidence: 'INFERRED',
|
|
552
|
+
// Walked the class hierarchy, which is same-file only when
|
|
553
|
+
// the declaring class is; the honest mechanism for a
|
|
554
|
+
// hierarchy walk is the repo-wide method table.
|
|
555
|
+
resolved_via: 'name-lookup',
|
|
556
|
+
provider: 'native',
|
|
557
|
+
});
|
|
443
558
|
} else {
|
|
444
559
|
// enclosing class extends an out-of-repo base → honest AMBIGUOUS
|
|
445
560
|
const cands = methodCandidates(r.targetName, ex.lang);
|
|
@@ -448,6 +563,8 @@ export function buildGraph(files: readonly SourceFile[], extracts: readonly File
|
|
|
448
563
|
target: ambiguousTarget(cands, r.targetName),
|
|
449
564
|
relation: 'calls',
|
|
450
565
|
confidence: 'AMBIGUOUS',
|
|
566
|
+
resolved_via: 'dynamic',
|
|
567
|
+
provider: 'native',
|
|
451
568
|
// The enclosing class extends a base outside this
|
|
452
569
|
// repository, so the method is not findable HERE — a
|
|
453
570
|
// different cause from a call whose receiver is unknown,
|
|
@@ -486,6 +603,8 @@ export function buildGraph(files: readonly SourceFile[], extracts: readonly File
|
|
|
486
603
|
target: ambiguousTarget(cands, r.targetName),
|
|
487
604
|
relation: 'calls',
|
|
488
605
|
confidence: 'AMBIGUOUS',
|
|
606
|
+
resolved_via: 'dynamic',
|
|
607
|
+
provider: 'native',
|
|
489
608
|
// Receiver type unknown: `$obj->m()` or an unresolved `C::m()`.
|
|
490
609
|
// Resolving this needs type inference the extractor does not do,
|
|
491
610
|
// which is exactly what 1.2 has to decide about rather than assume.
|
|
@@ -494,6 +613,52 @@ export function buildGraph(files: readonly SourceFile[], extracts: readonly File
|
|
|
494
613
|
});
|
|
495
614
|
}
|
|
496
615
|
|
|
616
|
+
// DERIVED `tests` edges (3.2).
|
|
617
|
+
//
|
|
618
|
+
// A test file that imports an in-repo symbol from a NON-test file is
|
|
619
|
+
// asserted to test it. Derived here rather than extracted because the
|
|
620
|
+
// predicate is a property of the two PATHS, which no grammar can see, and
|
|
621
|
+
// because `buildGraph` already holds every `imports` edge plus the node
|
|
622
|
+
// table needed to reject cross-test and out-of-repo targets.
|
|
623
|
+
//
|
|
624
|
+
// Three exclusions, each of which would otherwise manufacture a false
|
|
625
|
+
// `tests` edge:
|
|
626
|
+
// · target outside the repository (`external:*` / `symbol:*`) — there is
|
|
627
|
+
// no subject to test;
|
|
628
|
+
// · target in another TEST file — a shared test helper is not a subject;
|
|
629
|
+
// · target is the subject FILE node rather than a symbol in it, which is
|
|
630
|
+
// kept: a test that imports a module tests that module, and the file
|
|
631
|
+
// node is the only handle a caller has for a module with no exported
|
|
632
|
+
// symbol the extractor named;
|
|
633
|
+
// · THE SOURCE IMPORT IS ITSELF A GUESS. Added after an independent review
|
|
634
|
+
// demonstrated the laundering: a PHP `use Two\Mailer` with no
|
|
635
|
+
// `composer.json` binds by BASE NAME, so the `imports` edge is
|
|
636
|
+
// `name-lookup` and can point at `One\Mailer` — and a `tests` edge
|
|
637
|
+
// derived from it, stamped `test-import`, made that wrong target
|
|
638
|
+
// TRUSTWORTHY to every consumer of the accepted-edge filter. Measured
|
|
639
|
+
// consequences on a two-namespace fixture: `tests-for One\Mailer`
|
|
640
|
+
// named a test that does not test it, `tests-for Two\Mailer` reported
|
|
641
|
+
// none, `untested` inverted the pair, and `dead` called the tested class
|
|
642
|
+
// dead. A derived edge may never be more trustworthy than the edge it
|
|
643
|
+
// was derived from.
|
|
644
|
+
const nodeSourceFile = new Map(nodes.map((n) => [n.id, n.source_file]));
|
|
645
|
+
for (const e of [...edges]) {
|
|
646
|
+
if (e.relation !== 'imports') continue;
|
|
647
|
+
if (!isStatedResolution(e.resolved_via)) continue;
|
|
648
|
+
const from = nodeSourceFile.get(e.source);
|
|
649
|
+
if (from === undefined || !isTestFile(from)) continue;
|
|
650
|
+
const to = nodeSourceFile.get(e.target);
|
|
651
|
+
if (to === undefined || isTestFile(to)) continue;
|
|
652
|
+
emit({
|
|
653
|
+
source: e.source,
|
|
654
|
+
target: e.target,
|
|
655
|
+
relation: 'tests',
|
|
656
|
+
confidence: 'INFERRED',
|
|
657
|
+
resolved_via: 'test-import',
|
|
658
|
+
provider: 'native',
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
|
|
497
662
|
// deterministic ordering
|
|
498
663
|
nodes.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
499
664
|
edges.sort((a, b) => {
|
|
@@ -559,6 +724,38 @@ function ambiguousTarget(candidates: readonly string[], targetName: string): str
|
|
|
559
724
|
return `symbol:${targetName}`;
|
|
560
725
|
}
|
|
561
726
|
|
|
727
|
+
/**
|
|
728
|
+
* Is `relPath` a test file?
|
|
729
|
+
*
|
|
730
|
+
* A deterministic PATH predicate — no content read, no heuristic on the file's
|
|
731
|
+
* body. It is the input to the derived `tests` relation (3.2), so it has to be
|
|
732
|
+
* conservative in one direction only: a subject wrongly classified as a test
|
|
733
|
+
* silently loses its `tests` edges, while a test wrongly classified as a
|
|
734
|
+
* subject would make a test file look like a tested surface. Both are wrong;
|
|
735
|
+
* the second is worse, because `untested` would then report a real subject as
|
|
736
|
+
* covered.
|
|
737
|
+
*
|
|
738
|
+
* The four conventions are the ones this launch set actually uses, and each is
|
|
739
|
+
* anchored rather than substring-matched so a directory called
|
|
740
|
+
* `src/latest/thing.ts` is not a test:
|
|
741
|
+
*
|
|
742
|
+
* · a `tests/` or `test/` path segment (this repository's own layout);
|
|
743
|
+
* · a `__tests__/` segment (the JS convention);
|
|
744
|
+
* · a `.test.` / `.spec.` filename infix (Vitest / Jest / Pest-JS);
|
|
745
|
+
* · a `Test.php` / `test.php` filename suffix, or a `_test.php` infix
|
|
746
|
+
* (PHPUnit's `*Test.php` and Go-style `_test` both appear in PHP trees).
|
|
747
|
+
*/
|
|
748
|
+
export function isTestFile(relPath: string): boolean {
|
|
749
|
+
const p = relPath.split(path.sep).join('/');
|
|
750
|
+
const segments = p.split('/');
|
|
751
|
+
if (segments.some((seg) => seg === 'tests' || seg === 'test' || seg === '__tests__')) return true;
|
|
752
|
+
const base = segments[segments.length - 1] ?? '';
|
|
753
|
+
if (/\.(test|spec)\./i.test(base)) return true;
|
|
754
|
+
if (/(^|[._-])test\.php$/i.test(base)) return true;
|
|
755
|
+
if (/Test\.php$/.test(base)) return true;
|
|
756
|
+
return false;
|
|
757
|
+
}
|
|
758
|
+
|
|
562
759
|
export function serializeGraph(g: CodeGraph): string {
|
|
563
760
|
const key_order: (keyof CodeGraph)[] = [
|
|
564
761
|
'schema_version',
|
|
@@ -583,6 +780,11 @@ export function serializeGraph(g: CodeGraph): string {
|
|
|
583
780
|
target: e.target,
|
|
584
781
|
relation: e.relation,
|
|
585
782
|
confidence: e.confidence,
|
|
783
|
+
// Unconditional, unlike the two optional fields below: these are
|
|
784
|
+
// required on every edge, so emitting them conditionally would make
|
|
785
|
+
// an absent field mean "not set" instead of being impossible.
|
|
786
|
+
resolved_via: e.resolved_via,
|
|
787
|
+
provider: e.provider,
|
|
586
788
|
};
|
|
587
789
|
// Ordered before `candidates` deliberately: the reason is the coarser
|
|
588
790
|
// axis and a reader scanning the serialized graph sees WHY before HOW
|
|
@@ -684,7 +886,7 @@ export async function buildFromRepo(
|
|
|
684
886
|
nextSidecar.files[f.path] = { hash, extract };
|
|
685
887
|
}
|
|
686
888
|
|
|
687
|
-
const graph = buildGraph(files, extracts);
|
|
889
|
+
const graph = buildGraph(files, extracts, readResolutionConfig(root));
|
|
688
890
|
const json = serializeGraph(graph);
|
|
689
891
|
if (cachePath) {
|
|
690
892
|
write_atomic(cachePath, json);
|