@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
|
@@ -37,4 +37,112 @@ export function writesThinFiles(mode) {
|
|
|
37
37
|
export function deliversBodies(mode) {
|
|
38
38
|
return mode === 'delivery';
|
|
39
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* The hosts a thinning mode may actually thin.
|
|
42
|
+
*
|
|
43
|
+
* Deliberately NOT "every host id the package knows". `condense`'s `TOOL_DIRS`
|
|
44
|
+
* writes a per-rule tree for exactly three hosts, so those are the only ids for
|
|
45
|
+
* which `lean_projection.hosts` can mean anything: naming `windsurf` there would
|
|
46
|
+
* be a request to thin a single concatenated file that has no per-rule stub
|
|
47
|
+
* shape, and naming `codex` a request to thin a tree the installer writes and
|
|
48
|
+
* `condense` never touches. Accepting such an id silently would leave the
|
|
49
|
+
* operator believing a host was scoped when nothing reads the entry.
|
|
50
|
+
*/
|
|
51
|
+
export const THINNABLE_HOSTS = ['claude-code', 'cursor', 'cline'];
|
|
52
|
+
/**
|
|
53
|
+
* The shipped default host set for a thinning mode.
|
|
54
|
+
*
|
|
55
|
+
* One host, and it is the one host that both binds `pre_tool_use` and honours a
|
|
56
|
+
* deny (`docs/enforcement-by-host.md:18-28`) — i.e. the only host where the
|
|
57
|
+
* delivery concern's verdict is acted on. Widening this is a decision, never a
|
|
58
|
+
* default: see `resolveLeanProjectionHosts`, which never widens implicitly.
|
|
59
|
+
*/
|
|
60
|
+
export const DEFAULT_LEAN_PROJECTION_HOSTS = ['claude-code'];
|
|
61
|
+
/**
|
|
62
|
+
* Resolve a configured `lean_projection.hosts` list.
|
|
63
|
+
*
|
|
64
|
+
* Three properties, each of which is a failure this repository has seen in a
|
|
65
|
+
* neighbouring config surface:
|
|
66
|
+
*
|
|
67
|
+
* 1. **Absent means the default, never "all".** An empty or missing list
|
|
68
|
+
* resolves to `DEFAULT_LEAN_PROJECTION_HOSTS`. A key nobody set must not
|
|
69
|
+
* thin every host — that is D1, the defect this axis exists to repair.
|
|
70
|
+
* 2. **The set never widens implicitly.** Every id is checked against
|
|
71
|
+
* `THINNABLE_HOSTS`; anything else is dropped. A typo cannot enrol a host,
|
|
72
|
+
* and neither can a real host id that has no per-rule tree.
|
|
73
|
+
* 3. **A drop is reported, not swallowed.** The caller gets the ids and the
|
|
74
|
+
* reason so it can print them. Silently ignoring an entry an operator wrote
|
|
75
|
+
* is how a setting comes to mean nothing while looking configured.
|
|
76
|
+
*
|
|
77
|
+
* A configured list that resolves to NOTHING keeps `usedDefault: false` and an
|
|
78
|
+
* empty `hosts` — that is "thin no host", which is a legitimate and safe answer
|
|
79
|
+
* and must not fall back to the default. Falling back there would turn a
|
|
80
|
+
* fully-typo'd list into a Claude Code flip the operator never asked for.
|
|
81
|
+
*
|
|
82
|
+
* WHICH FILES REACH THIS AT ALL, stated because R2 finding 6 read the drop path
|
|
83
|
+
* as unreachable: `agent-settings.schema.json` and the wizard's Zod schema both
|
|
84
|
+
* constrain the items to an `enum`, so an out-of-vocabulary id written through
|
|
85
|
+
* either surface is a hard validation FAILURE and never arrives here. What
|
|
86
|
+
* arrives here is a HAND-EDITED `.agent-settings.yml` — the file
|
|
87
|
+
* `condense._lean_projection_settings` and `hooks/rule_inject_hook.gateOpen`
|
|
88
|
+
* both read directly, with no validation step in between. The two layers are
|
|
89
|
+
* defence in depth, not one contract stated twice: the schema is the authoring
|
|
90
|
+
* gate, this function is the read-time gate, and the drop wording below exists
|
|
91
|
+
* for the file the schema never saw.
|
|
92
|
+
*/
|
|
93
|
+
export function resolveLeanProjectionHosts(raw) {
|
|
94
|
+
const known = new Set(THINNABLE_HOSTS);
|
|
95
|
+
if (!Array.isArray(raw) || raw.length === 0) {
|
|
96
|
+
return { hosts: [...DEFAULT_LEAN_PROJECTION_HOSTS], dropped: [], usedDefault: true };
|
|
97
|
+
}
|
|
98
|
+
const hosts = [];
|
|
99
|
+
const dropped = [];
|
|
100
|
+
for (const entry of raw) {
|
|
101
|
+
const id = typeof entry === 'string' ? entry.trim().toLowerCase() : '';
|
|
102
|
+
if (id === '') {
|
|
103
|
+
dropped.push({ id: String(entry), reason: 'unknown' });
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (!known.has(id)) {
|
|
107
|
+
// `not-thinnable` is reserved for an id this package genuinely knows
|
|
108
|
+
// as a host but which owns no per-rule tree. Everything else is a
|
|
109
|
+
// typo or an invention, and the two deserve different wording
|
|
110
|
+
// because the operator's next action differs: fix the spelling, or
|
|
111
|
+
// learn that the host cannot be thinned at all.
|
|
112
|
+
dropped.push({ id, reason: KNOWN_NON_THINNABLE_HOSTS.has(id) ? 'not-thinnable' : 'unknown' });
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (!hosts.includes(id))
|
|
116
|
+
hosts.push(id);
|
|
117
|
+
}
|
|
118
|
+
hosts.sort();
|
|
119
|
+
return { hosts, dropped, usedDefault: false };
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Host ids the package ships surfaces for that own no per-rule rule tree.
|
|
123
|
+
*
|
|
124
|
+
* Only used to word a drop precisely; it grants nothing. Sourced from
|
|
125
|
+
* `condense._ALL_TOOLS` plus the two hosts outside it that the host table
|
|
126
|
+
* carries (`codex`, `cowork`).
|
|
127
|
+
*/
|
|
128
|
+
const KNOWN_NON_THINNABLE_HOSTS = new Set([
|
|
129
|
+
'claude-desktop', 'augment', 'copilot', 'windsurf', 'gemini', 'codex', 'cowork',
|
|
130
|
+
]);
|
|
131
|
+
/** One line per dropped id, for a caller that prints warnings. Empty when nothing was dropped. */
|
|
132
|
+
export function describeDroppedHosts(res) {
|
|
133
|
+
return res.dropped.map(({ id, reason }) => reason === 'not-thinnable'
|
|
134
|
+
? `lean_projection.hosts: "${id}" is a known host but has no per-rule rule tree — dropped`
|
|
135
|
+
: `lean_projection.hosts: "${id}" is not a thinnable host id — dropped`);
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Does the projector write stubs for this host, under this mode and host set?
|
|
139
|
+
*
|
|
140
|
+
* The single predicate `condense` calls. Mode is checked FIRST and on purpose:
|
|
141
|
+
* with `mode` unset the answer is `false` for every host regardless of what
|
|
142
|
+
* `hosts` says, so a `hosts:` list left behind after a rollback to `eager-all`
|
|
143
|
+
* thins nothing.
|
|
144
|
+
*/
|
|
145
|
+
export function thinsHost(mode, hosts, hostId) {
|
|
146
|
+
return writesThinFiles(mode) && hosts.includes(hostId);
|
|
147
|
+
}
|
|
40
148
|
//# sourceMappingURL=lean_projection_mode.js.map
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE single trigger-matcher implementation for `dist/router.json`.
|
|
3
|
+
*
|
|
4
|
+
* Parity by construction: every surface that answers "which rules fire on
|
|
5
|
+
* this prompt?" — router replay (`router_telemetry.ts`), trigger-coverage
|
|
6
|
+
* CI (`trigger_coverage.ts`), `agent-config explain route`, and
|
|
7
|
+
* `agent-config route:explain` — MUST import the matchers from this module.
|
|
8
|
+
* A second implementation of these semantics anywhere in the tree is a
|
|
9
|
+
* violation (enforced by `tests/scripts/router_match_parity.test.ts`); the
|
|
10
|
+
* historical divergence this kills was `cmd_explain.ts` shipping unanchored
|
|
11
|
+
* keyword matching while the replay path was anchored.
|
|
12
|
+
*
|
|
13
|
+
* Canonical semantics: `docs/contracts/rule-router.md` § `triggers:` shape
|
|
14
|
+
* (the trigger-semantics table — anchored `keyword`, unanchored `phrase`,
|
|
15
|
+
* `command` prefix, `path_prefix` / `file_pattern` over open files).
|
|
16
|
+
*
|
|
17
|
+
* Extracted verbatim from `router_telemetry.ts` (which re-exports the same
|
|
18
|
+
* names for existing importers). Behaviour is pinned by the routing-matrix
|
|
19
|
+
* and trigger-coverage suites — do not "fix" quirks here.
|
|
20
|
+
*/
|
|
21
|
+
// ── fnmatch (Python fnmatch.fnmatch) ─────────────────────────────────────
|
|
22
|
+
/**
|
|
23
|
+
* Translate a Unix shell pattern into a RegExp, matching CPython's
|
|
24
|
+
* `fnmatch.translate`. `*` → `.*`, `?` → `.`, `[...]` char classes
|
|
25
|
+
* preserved (with `!` → `^` negation), everything else escaped.
|
|
26
|
+
* Matching is case-sensitive on POSIX (the corpus paths are POSIX).
|
|
27
|
+
*/
|
|
28
|
+
function _fnmatchToRegExp(pat) {
|
|
29
|
+
let res = "";
|
|
30
|
+
let i = 0;
|
|
31
|
+
const n = pat.length;
|
|
32
|
+
while (i < n) {
|
|
33
|
+
const c = pat[i];
|
|
34
|
+
i += 1;
|
|
35
|
+
if (c === "*") {
|
|
36
|
+
res += ".*";
|
|
37
|
+
}
|
|
38
|
+
else if (c === "?") {
|
|
39
|
+
res += ".";
|
|
40
|
+
}
|
|
41
|
+
else if (c === "[") {
|
|
42
|
+
let j = i;
|
|
43
|
+
if (j < n && pat[j] === "!") {
|
|
44
|
+
j += 1;
|
|
45
|
+
}
|
|
46
|
+
if (j < n && pat[j] === "]") {
|
|
47
|
+
j += 1;
|
|
48
|
+
}
|
|
49
|
+
while (j < n && pat[j] !== "]") {
|
|
50
|
+
j += 1;
|
|
51
|
+
}
|
|
52
|
+
if (j >= n) {
|
|
53
|
+
res += "\\[";
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
let stuff = pat.slice(i, j);
|
|
57
|
+
if (!stuff.includes("-")) {
|
|
58
|
+
stuff = stuff.replace(/\\/g, "\\\\");
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
// Faithful port of CPython's dash-handling in fnmatch.translate:
|
|
62
|
+
// split the class body on `-` (bounded to [i, j)), escaping each
|
|
63
|
+
// chunk, then re-join with `-` so ranges survive.
|
|
64
|
+
const chunks = [];
|
|
65
|
+
let k = pat[i] === "!" ? i + 2 : i + 1;
|
|
66
|
+
let start = i;
|
|
67
|
+
while (true) {
|
|
68
|
+
const rel = pat.slice(k, j).indexOf("-");
|
|
69
|
+
if (rel < 0) {
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
const idx = k + rel;
|
|
73
|
+
chunks.push(pat.slice(start, idx));
|
|
74
|
+
start = idx + 1;
|
|
75
|
+
k = idx + 3;
|
|
76
|
+
}
|
|
77
|
+
chunks.push(pat.slice(start, j));
|
|
78
|
+
stuff = chunks
|
|
79
|
+
.map((s) => s.replace(/\\/g, "\\\\").replace(/-/g, "\\-"))
|
|
80
|
+
.join("-");
|
|
81
|
+
}
|
|
82
|
+
// Escape regex set-meta chars except backslash/dash already handled.
|
|
83
|
+
stuff = stuff.replace(/([&~|])/g, "\\$1");
|
|
84
|
+
i = j + 1;
|
|
85
|
+
if (stuff.startsWith("!")) {
|
|
86
|
+
stuff = "^" + stuff.slice(1);
|
|
87
|
+
}
|
|
88
|
+
else if (stuff.startsWith("^") || stuff.startsWith("[")) {
|
|
89
|
+
stuff = "\\" + stuff;
|
|
90
|
+
}
|
|
91
|
+
res += `[${stuff}]`;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
res += c.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return new RegExp(`^(?:${res})$`, "s");
|
|
99
|
+
}
|
|
100
|
+
export function _fnmatch(name, pat) {
|
|
101
|
+
return _fnmatchToRegExp(pat).test(name);
|
|
102
|
+
}
|
|
103
|
+
// ── Trigger matching ─────────────────────────────────────────────────────
|
|
104
|
+
/** Apply one trigger to a prompt + context; return True on match. */
|
|
105
|
+
/** Unicode word character — letters, digits, underscore (incl. umlauts). */
|
|
106
|
+
const _WORD_CHAR = /[\p{L}\p{N}_]/u;
|
|
107
|
+
function _isWordChar(c) {
|
|
108
|
+
return c !== undefined && _WORD_CHAR.test(c);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Word-boundary-anchored keyword match (road-to-tested-routing Phase 3).
|
|
112
|
+
*
|
|
113
|
+
* A `keyword` occurrence counts only when its word-character edges sit on
|
|
114
|
+
* word boundaries: `ac` no longer fires inside "black", `plan` no longer
|
|
115
|
+
* fires inside "planet". Two deliberate reliefs:
|
|
116
|
+
*
|
|
117
|
+
* - An edge that is itself a NON-word character (e.g. `__()`, `/image:`,
|
|
118
|
+
* `trans(`, an emoji) carries no boundary requirement on that side —
|
|
119
|
+
* punctuation-shaped keywords keep their exact substring semantics.
|
|
120
|
+
* - The right boundary accepts one optional plural `s` ("icons", "options",
|
|
121
|
+
* "secrets" still fire their singular keyword). Richer inflection (German
|
|
122
|
+
* verb endings: "implementiere", "committen") is a DOCUMENTED accepted
|
|
123
|
+
* recall cost of anchoring — the matrices carry standalone-token phrasings
|
|
124
|
+
* for those languages instead.
|
|
125
|
+
*
|
|
126
|
+
* `phrase` deliberately stays unanchored substring; since this change the
|
|
127
|
+
* two trigger kinds genuinely differ.
|
|
128
|
+
*/
|
|
129
|
+
export function keyword_matches_anchored(prompt_lower, kw_lower) {
|
|
130
|
+
if (kw_lower === "") {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
const needLeft = _isWordChar(kw_lower[0]);
|
|
134
|
+
const needRight = _isWordChar(kw_lower[kw_lower.length - 1]);
|
|
135
|
+
let idx = prompt_lower.indexOf(kw_lower);
|
|
136
|
+
while (idx !== -1) {
|
|
137
|
+
const leftOk = !needLeft || !_isWordChar(prompt_lower[idx - 1]);
|
|
138
|
+
let rightOk = true;
|
|
139
|
+
if (needRight) {
|
|
140
|
+
let end = idx + kw_lower.length;
|
|
141
|
+
if (prompt_lower[end] === "s") {
|
|
142
|
+
end += 1; // optional plural `s`
|
|
143
|
+
}
|
|
144
|
+
rightOk = !_isWordChar(prompt_lower[end]);
|
|
145
|
+
}
|
|
146
|
+
if (leftOk && rightOk) {
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
idx = prompt_lower.indexOf(kw_lower, idx + 1);
|
|
150
|
+
}
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
export function trigger_matches(trigger, prompt, open_files, command) {
|
|
154
|
+
const prompt_lower = prompt.toLowerCase();
|
|
155
|
+
if ("keyword" in trigger) {
|
|
156
|
+
return keyword_matches_anchored(prompt_lower, String(trigger["keyword"]).toLowerCase());
|
|
157
|
+
}
|
|
158
|
+
if ("phrase" in trigger) {
|
|
159
|
+
return prompt_lower.includes(String(trigger["phrase"]).toLowerCase());
|
|
160
|
+
}
|
|
161
|
+
if ("command" in trigger) {
|
|
162
|
+
if (!command) {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
return command.startsWith(String(trigger["command"]));
|
|
166
|
+
}
|
|
167
|
+
if ("path_prefix" in trigger) {
|
|
168
|
+
if (!open_files) {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
const pref = String(trigger["path_prefix"]);
|
|
172
|
+
for (const p of open_files) {
|
|
173
|
+
if (String(p).startsWith(pref)) {
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
if ("file_pattern" in trigger) {
|
|
180
|
+
if (!open_files) {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
const pat = String(trigger["file_pattern"]);
|
|
184
|
+
for (const p of open_files) {
|
|
185
|
+
if (_fnmatch(String(p), pat)) {
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
export function _asRuleList(value) {
|
|
194
|
+
if (!Array.isArray(value)) {
|
|
195
|
+
return [];
|
|
196
|
+
}
|
|
197
|
+
return value;
|
|
198
|
+
}
|
|
199
|
+
export function _asTriggerList(value) {
|
|
200
|
+
if (!Array.isArray(value)) {
|
|
201
|
+
return [];
|
|
202
|
+
}
|
|
203
|
+
return value;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Return the matched-triggers + activated-rules for one prompt.
|
|
207
|
+
*
|
|
208
|
+
* Kernel rules are always active. tier_1 always considered. tier_2
|
|
209
|
+
* only considered when `profile == 'full'`.
|
|
210
|
+
*/
|
|
211
|
+
export function match_prompt(router, prompt, profile = "full", open_files, command) {
|
|
212
|
+
const tiers = [
|
|
213
|
+
["tier_1", _asRuleList(router["tier_1"])],
|
|
214
|
+
];
|
|
215
|
+
if (profile === "full") {
|
|
216
|
+
tiers.push(["tier_2", _asRuleList(router["tier_2"])]);
|
|
217
|
+
}
|
|
218
|
+
const matched_triggers = [];
|
|
219
|
+
const activated_rules = [];
|
|
220
|
+
for (const [tier_name, rules] of tiers) {
|
|
221
|
+
for (const rule of rules) {
|
|
222
|
+
const rule_id = rule["id"];
|
|
223
|
+
const rule_triggers = _asTriggerList(rule["triggers"]);
|
|
224
|
+
let rule_hit = false;
|
|
225
|
+
for (const trig of rule_triggers) {
|
|
226
|
+
if (trigger_matches(trig, prompt, open_files, command)) {
|
|
227
|
+
matched_triggers.push({ tier: tier_name, rule: rule_id, trigger: trig });
|
|
228
|
+
rule_hit = true;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (rule_hit) {
|
|
232
|
+
activated_rules.push({ tier: tier_name, rule: rule_id });
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
// Kernel rules are always active.
|
|
237
|
+
const kernel = Array.isArray(router["kernel"]) ? router["kernel"] : [];
|
|
238
|
+
for (const kid of kernel) {
|
|
239
|
+
activated_rules.push({ tier: "kernel", rule: kid });
|
|
240
|
+
}
|
|
241
|
+
return { matched_triggers, activated_rules };
|
|
242
|
+
}
|
|
243
|
+
//# sourceMappingURL=router_match.js.map
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule-injection core — THE single module both the offline model and the
|
|
3
|
+
* runtime concern read.
|
|
4
|
+
*
|
|
5
|
+
* WHY ONE MODULE. `road-to-trigger-delivered-rule-bodies` step 0.5 states the
|
|
6
|
+
* failure this prevents in as many words: *an experiment whose offline pricing
|
|
7
|
+
* and runtime delivery use different matchers measures nothing*. So the price /
|
|
8
|
+
* recall model (`src/scripts/model_rule_injection.ts`) and the delivery concern
|
|
9
|
+
* (`src/scripts/hooks/rule_inject_hook.ts`) both import THIS file, and neither
|
|
10
|
+
* re-implements selection, ordering, capping, or body loading.
|
|
11
|
+
*
|
|
12
|
+
* IT OWNS NO MATCHER. Trigger semantics belong to
|
|
13
|
+
* [`_lib/router_match.ts`](router_match.ts), which is already the single
|
|
14
|
+
* implementation for every surface that answers "which rules fire on this
|
|
15
|
+
* prompt?" and is pinned by `tests/scripts/router_match_parity.test.ts`. A
|
|
16
|
+
* second matcher here would be the violation that test exists to catch, so
|
|
17
|
+
* `matchTierRules` is a thin, deterministic wrapper over `match_prompt` and
|
|
18
|
+
* nothing more. Step 0.3's pre-registered comparison found no reason to write
|
|
19
|
+
* one — see `model_rule_injection.ts`'s header for the measured verdict.
|
|
20
|
+
*
|
|
21
|
+
* KERNEL IS NEVER INJECTED. The nine kernel rules are in standing context by
|
|
22
|
+
* definition; injecting one would be paying twice for a body that is already
|
|
23
|
+
* there. `match_prompt` returns kernel ids in `activated_rules` because kernel
|
|
24
|
+
* rules are always active — this module filters them out by construction rather
|
|
25
|
+
* than relying on a caller to remember.
|
|
26
|
+
*
|
|
27
|
+
* IT IMPORTS NO TOKENIZER, AND THAT IS A HOT-PATH FACT RATHER THAN A STYLE
|
|
28
|
+
* CHOICE. `_lib/token_count.ts` resolves `js-tiktoken` AT MODULE LOAD, and the
|
|
29
|
+
* concern that imports this file is statically reachable from
|
|
30
|
+
* `concern_registry.ts` — so importing it here made every hook dispatch on
|
|
31
|
+
* every slot pay a tokenizer load for a concern that is default-OFF and emits
|
|
32
|
+
* nothing. Measured on this tree: unbinding the concern moved the
|
|
33
|
+
* `pre_tool_use` p95 from 202 ms to 196 ms, and the CI latency gate went red on
|
|
34
|
+
* the branch that introduced it while passing on main.
|
|
35
|
+
*
|
|
36
|
+
* So the runtime cap is in BYTES. That is not a weaker bound — it is the same
|
|
37
|
+
* bound in the unit the budget actually enforces: `hook-token-budget.json`
|
|
38
|
+
* measures "bytes of concern stdout payload fields", so the cap and its
|
|
39
|
+
* registered row are now the same number instead of two units that need a
|
|
40
|
+
* conversion factor to compare. The offline model keeps exact-BPE tokens for
|
|
41
|
+
* its price table, where the tokenizer is the point and nothing is on a hot
|
|
42
|
+
* path.
|
|
43
|
+
*
|
|
44
|
+
* DETERMINISM IS THE CONTRACT, and the collision cases are the reason it needs
|
|
45
|
+
* stating. Two triggers on the SAME rule collapse to one entry (a rule is
|
|
46
|
+
* delivered once, however many of its triggers fired) while the trigger count
|
|
47
|
+
* survives as `score`. Two DIFFERENT rules both firing are returned in router
|
|
48
|
+
* declaration order, tier_1 before tier_2 — never in match order, which would
|
|
49
|
+
* make the output depend on trigger authoring order inside a rule. Capping
|
|
50
|
+
* sorts by descending score with router order as the tie-break, so the same
|
|
51
|
+
* prompt against the same router always yields the same bytes.
|
|
52
|
+
*/
|
|
53
|
+
import * as fs from 'node:fs';
|
|
54
|
+
import * as path from 'node:path';
|
|
55
|
+
import { match_prompt } from './router_match.js';
|
|
56
|
+
/** Kinds of trigger a prompt alone can never fire. */
|
|
57
|
+
export const PATH_TRIGGER_KINDS = ['path_prefix', 'file_pattern'];
|
|
58
|
+
export function loadRouter(repoRoot) {
|
|
59
|
+
const p = path.join(repoRoot, 'dist', 'router.json');
|
|
60
|
+
return JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
61
|
+
}
|
|
62
|
+
/** Kernel rule ids, as declared by the router. */
|
|
63
|
+
export function kernelIds(router) {
|
|
64
|
+
const k = Array.isArray(router['kernel']) ? router['kernel'] : [];
|
|
65
|
+
return new Set(k.map((x) => String(x)));
|
|
66
|
+
}
|
|
67
|
+
function tierEntries(router, tier) {
|
|
68
|
+
const raw = router[tier];
|
|
69
|
+
if (!Array.isArray(raw))
|
|
70
|
+
return [];
|
|
71
|
+
return raw.map((r) => ({
|
|
72
|
+
id: String(r['id'] ?? ''),
|
|
73
|
+
triggers: Array.isArray(r['triggers']) ? r['triggers'] : [],
|
|
74
|
+
}));
|
|
75
|
+
}
|
|
76
|
+
/** Every tier rule declared by the router, with its trigger list. */
|
|
77
|
+
export function allTierRules(router) {
|
|
78
|
+
const out = [];
|
|
79
|
+
for (const tier of ['tier_1', 'tier_2']) {
|
|
80
|
+
for (const e of tierEntries(router, tier))
|
|
81
|
+
out.push({ ...e, tier });
|
|
82
|
+
}
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
/** Rules whose triggers are ALL path-shaped — unreachable from a prompt alone. */
|
|
86
|
+
export function pathOnlyRuleIds(router) {
|
|
87
|
+
const ids = new Set();
|
|
88
|
+
for (const r of allTierRules(router)) {
|
|
89
|
+
if (r.triggers.length === 0)
|
|
90
|
+
continue;
|
|
91
|
+
const allPath = r.triggers.every((t) => PATH_TRIGGER_KINDS.some((k) => k in t));
|
|
92
|
+
if (allPath)
|
|
93
|
+
ids.add(r.id);
|
|
94
|
+
}
|
|
95
|
+
return ids;
|
|
96
|
+
}
|
|
97
|
+
/** Rules declaring at least one path-shaped trigger. */
|
|
98
|
+
export function pathCapableRuleIds(router) {
|
|
99
|
+
const ids = new Set();
|
|
100
|
+
for (const r of allTierRules(router)) {
|
|
101
|
+
if (r.triggers.some((t) => PATH_TRIGGER_KINDS.some((k) => k in t)))
|
|
102
|
+
ids.add(r.id);
|
|
103
|
+
}
|
|
104
|
+
return ids;
|
|
105
|
+
}
|
|
106
|
+
/** Rules declaring no trigger at all — cannot be delivered, must stay eager. */
|
|
107
|
+
export function triggerlessRuleIds(router) {
|
|
108
|
+
return allTierRules(router)
|
|
109
|
+
.filter((r) => r.triggers.length === 0)
|
|
110
|
+
.map((r) => r.id);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Which tier rules fire on this prompt (+ optional open files / command).
|
|
114
|
+
*
|
|
115
|
+
* Kernel is excluded. Order is router declaration order, tier_1 first.
|
|
116
|
+
*/
|
|
117
|
+
export function matchTierRules(router, prompt, openFiles, command) {
|
|
118
|
+
const kernel = kernelIds(router);
|
|
119
|
+
const res = match_prompt(router, prompt, 'full', openFiles, command);
|
|
120
|
+
const scores = new Map();
|
|
121
|
+
for (const mt of res.matched_triggers) {
|
|
122
|
+
const id = String(mt.rule ?? '');
|
|
123
|
+
if (id === '' || kernel.has(id))
|
|
124
|
+
continue;
|
|
125
|
+
scores.set(id, (scores.get(id) ?? 0) + 1);
|
|
126
|
+
}
|
|
127
|
+
const out = [];
|
|
128
|
+
let order = 0;
|
|
129
|
+
for (const r of allTierRules(router)) {
|
|
130
|
+
const s = scores.get(r.id);
|
|
131
|
+
if (s === undefined) {
|
|
132
|
+
order += 1;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
out.push({ id: r.id, tier: r.tier, score: s, order });
|
|
136
|
+
order += 1;
|
|
137
|
+
}
|
|
138
|
+
return out;
|
|
139
|
+
}
|
|
140
|
+
/** Where the projected body of a rule lives. */
|
|
141
|
+
export function ruleBodyPath(repoRoot, id) {
|
|
142
|
+
return path.join(repoRoot, 'dist', 'agent-src', 'rules', `${id}.md`);
|
|
143
|
+
}
|
|
144
|
+
/** The projected body, or `null` when the rule has no projected file. */
|
|
145
|
+
export function loadRuleBody(repoRoot, id) {
|
|
146
|
+
const p = ruleBodyPath(repoRoot, id);
|
|
147
|
+
if (!fs.existsSync(p))
|
|
148
|
+
return null;
|
|
149
|
+
return fs.readFileSync(p, 'utf8');
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* UTF-8 byte length — the runtime measure, dependency-free by design.
|
|
153
|
+
*
|
|
154
|
+
* Deliberately NOT a token count: see the header. Bytes are exact, need no
|
|
155
|
+
* tokenizer, and are the unit `hook-token-budget.json` enforces.
|
|
156
|
+
*/
|
|
157
|
+
export function bytesOf(text) {
|
|
158
|
+
return Buffer.byteLength(text, 'utf8');
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Apply the per-prompt BYTE cap.
|
|
162
|
+
*
|
|
163
|
+
* Highest score first, router order as tie-break; a rule whose body would push
|
|
164
|
+
* the running total past `capBytes` is dropped and the walk continues, so one
|
|
165
|
+
* oversized body cannot starve the rest. The returned `selected` list is
|
|
166
|
+
* re-sorted back into router order — cap order is a budgeting concern, delivery
|
|
167
|
+
* order is not.
|
|
168
|
+
*
|
|
169
|
+
* ONE selection for both callers. The offline model and the runtime concern
|
|
170
|
+
* call THIS function with THE SAME cap, so the set the price table prices is
|
|
171
|
+
* byte-for-byte the set the concern delivers. That is step 0.5's requirement,
|
|
172
|
+
* and expressing the cap in bytes makes it hold without a token/byte
|
|
173
|
+
* conversion sitting between the two arms.
|
|
174
|
+
*/
|
|
175
|
+
export function selectForInjection(repoRoot, matches, capBytes, shortlist) {
|
|
176
|
+
const bodyBytes = new Map();
|
|
177
|
+
for (const m of matches) {
|
|
178
|
+
const body = loadRuleBody(repoRoot, m.id);
|
|
179
|
+
bodyBytes.set(m.id, body === null ? 0 : bytesOf(body));
|
|
180
|
+
}
|
|
181
|
+
// STEP 6.3 — the optional lexical shortlist, as a TIE-BREAK and nothing more.
|
|
182
|
+
//
|
|
183
|
+
// Absent (the default), the cap order is exactly what it was: score, then
|
|
184
|
+
// router order. Present, it sits BETWEEN them, never above `score` — the
|
|
185
|
+
// matcher's verdict is read first and the shortlist only orders what the
|
|
186
|
+
// matcher already tied. Membership is untouched in both directions: a
|
|
187
|
+
// shortlisted id the matcher did not return is not in `matches` and so is
|
|
188
|
+
// never consulted, and no matched id is dropped for being unshortlisted
|
|
189
|
+
// (it sorts at Infinity, behind the shortlisted ones, and still competes
|
|
190
|
+
// for the cap). That is `_lib/lexical_shortlist.ts`'s "never decides
|
|
191
|
+
// alone" expressed at the one place the cap actually binds.
|
|
192
|
+
const rank = new Map();
|
|
193
|
+
for (const [i, id] of (shortlist ?? []).entries()) {
|
|
194
|
+
if (!rank.has(id))
|
|
195
|
+
rank.set(id, i);
|
|
196
|
+
}
|
|
197
|
+
const ranked = [...matches].sort((a, b) => b.score - a.score ||
|
|
198
|
+
(rank.get(a.id) ?? Infinity) - (rank.get(b.id) ?? Infinity) ||
|
|
199
|
+
a.order - b.order);
|
|
200
|
+
const selected = [];
|
|
201
|
+
const dropped = [];
|
|
202
|
+
let total = 0;
|
|
203
|
+
for (const m of ranked) {
|
|
204
|
+
const t = bodyBytes.get(m.id) ?? 0;
|
|
205
|
+
if (total + t > capBytes && selected.length > 0) {
|
|
206
|
+
dropped.push(m);
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
selected.push(m);
|
|
210
|
+
total += t;
|
|
211
|
+
}
|
|
212
|
+
selected.sort((a, b) => a.order - b.order);
|
|
213
|
+
dropped.sort((a, b) => a.order - b.order);
|
|
214
|
+
return { selected, dropped, bytes: total, bodyBytes };
|
|
215
|
+
}
|
|
216
|
+
//# sourceMappingURL=rule_injection.js.map
|