@event4u/agent-config 9.12.0 → 9.14.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/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +110 -238
- package/README.md +1 -1
- package/dist/agent-src/commands/optimize/deep.md +147 -0
- package/dist/agent-src/commands/optimize.md +12 -6
- package/dist/agent-src/rules/active-remediation.md +0 -2
- package/dist/agent-src/rules/architecture.md +0 -1
- package/dist/agent-src/rules/artifact-drafting-protocol.md +0 -4
- package/dist/agent-src/rules/autonomous-execution.md +1 -2
- package/dist/agent-src/rules/broken-access-control.md +0 -2
- package/dist/agent-src/rules/code-comment-discipline.md +0 -3
- package/dist/agent-src/rules/code-provenance.md +0 -3
- package/dist/agent-src/rules/communication-through-line.md +2 -3
- package/dist/agent-src/rules/content-quoting-floor.md +0 -2
- package/dist/agent-src/rules/context-hygiene.md +1 -3
- package/dist/agent-src/rules/decision-revisit-gate.md +0 -2
- package/dist/agent-src/rules/design-fidelity.md +29 -0
- package/dist/agent-src/rules/domain-adoption-policy.md +0 -3
- package/dist/agent-src/rules/downstream-changes.md +10 -1
- package/dist/agent-src/rules/external-reference-deep-dive.md +0 -7
- package/dist/agent-src/rules/fast-path-marker-visibility.md +0 -1
- package/dist/agent-src/rules/git-history-discipline.md +0 -7
- package/dist/agent-src/rules/guidelines.md +0 -2
- package/dist/agent-src/rules/improve-before-implement.md +4 -2
- package/dist/agent-src/rules/invite-challenge.md +0 -4
- package/dist/agent-src/rules/linked-projects-onboarding-gate.md +0 -2
- package/dist/agent-src/rules/markdown-safe-codeblocks.md +0 -1
- package/dist/agent-src/rules/minimal-safe-diff.md +2 -2
- package/dist/agent-src/rules/missing-tool-handling.md +0 -1
- package/dist/agent-src/rules/no-attribution-footers.md +0 -3
- package/dist/agent-src/rules/no-decorative-emojis-in-git-surfaces.md +8 -5
- package/dist/agent-src/rules/no-pr-progress-comments.md +1 -3
- package/dist/agent-src/rules/no-roadmap-references.md +0 -2
- package/dist/agent-src/rules/output-discipline.md +0 -1
- package/dist/agent-src/rules/prefer-enums-over-literals.md +0 -2
- package/dist/agent-src/rules/preservation-guard.md +0 -3
- package/dist/agent-src/rules/role-mode-adherence.md +0 -1
- package/dist/agent-src/rules/secret-vcs-guard.md +0 -2
- package/dist/agent-src/rules/senior-engineering-discipline.md +0 -2
- package/dist/agent-src/rules/session-canary.md +0 -2
- package/dist/agent-src/rules/size-enforcement.md +0 -5
- package/dist/agent-src/rules/source-confidentiality.md +0 -3
- package/dist/agent-src/rules/source-discovery-gate.md +0 -2
- package/dist/agent-src/rules/think-before-action.md +3 -3
- package/dist/agent-src/rules/token-budget-discipline.md +0 -1
- package/dist/agent-src/rules/token-efficiency.md +1 -2
- package/dist/agent-src/rules/tool-safety.md +0 -1
- package/dist/agent-src/rules/user-interaction.md +0 -3
- package/dist/agent-src/rules/user-interrupt-priority.md +0 -1
- package/dist/agent-src/skills/agent-docs-writing/SKILL.md +8 -0
- package/dist/agent-src/skills/analysis-skill-router/SKILL.md +55 -86
- package/dist/agent-src/skills/character-consistency/SKILL.md +7 -4
- package/dist/agent-src/skills/decision-record/SKILL.md +8 -0
- package/dist/agent-src/skills/design-review/SKILL.md +11 -0
- package/dist/agent-src/skills/design-system-capture/reference/design-system-json.md +8 -0
- package/dist/agent-src/skills/existing-ui-audit/SKILL.md +34 -3
- package/dist/agent-src/skills/fe-design/SKILL.md +10 -0
- package/dist/agent-src/skills/motion-choreographer/SKILL.md +7 -4
- package/dist/agent-src/skills/overbuild-review-lens/SKILL.md +201 -0
- package/dist/agent-src/skills/performance/SKILL.md +14 -0
- package/dist/agent-src/skills/pixar-storyteller/SKILL.md +8 -5
- package/dist/agent-src/skills/scene-expander/SKILL.md +9 -6
- package/dist/agent-src/skills/supply-chain-intake/SKILL.md +10 -0
- package/dist/agent-src/skills/universal-project-analysis/SKILL.md +14 -37
- package/dist/agent-src/skills/video-director/SKILL.md +9 -6
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +154 -13
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/design.ts +118 -0
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/polish.ts +40 -0
- package/dist/agent-src/templates/scripts/work_engine/state.ts +55 -0
- package/dist/cli-delegate/{chunk-QFKQLQ7X.js → chunk-GCVFLBP2.js} +399 -365
- package/dist/cli-delegate/cmd_export.js +1 -1
- package/dist/cli-delegate/cmd_prune.js +1 -1
- package/dist/cli-delegate/cmd_settings_migrate.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +1 -1
- package/dist/cli-delegate/cmd_uninstall.js +1 -1
- package/dist/cli-delegate/cmd_validate.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +123 -73
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +4 -4
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +10 -8
- package/dist/discovery/trust-report.md +3 -3
- package/dist/discovery/workspaces.json +8 -6
- package/dist/hooks/dispatch.js +174 -13
- package/dist/install/install.mjs +648 -614
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +783 -507
- package/dist/router.json +1 -1
- package/dist/scripts/condense.js +2 -2
- package/docs/MIGRATION.md +1 -1
- package/docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md +71 -0
- package/docs/guidelines/agent-infra/minimal-safe-diff-mechanics.md +24 -1
- package/docs/guidelines/code-clarity.md +9 -2
- package/docs/guidelines/design-fidelity-mechanics.md +44 -0
- package/llms.txt +63 -27
- package/package.json +4 -4
- package/src/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +154 -13
- package/src/agent-src/templates/scripts/work_engine/directives/ui/design.ts +118 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/polish.ts +40 -0
- package/src/agent-src/templates/scripts/work_engine/state.ts +55 -0
- package/src/config/agent-settings.template.yml +8 -0
- package/src/scripts/_lib/counted_probe.ts +142 -0
- package/src/scripts/_lib/exec_evidence.ts +5 -0
- package/src/scripts/_lib/overbuild_lens_contract.ts +196 -0
- package/src/scripts/_lib/release_scope.ts +207 -0
- package/src/scripts/_lib/scoped_projection.ts +212 -0
- package/src/scripts/ai_council/stance_tally.ts +37 -2
- package/src/scripts/audit_auto_rules.ts +9 -9
- package/src/scripts/audit_likelihood.ts +3 -3
- package/src/scripts/audit_overlap.ts +2 -2
- package/src/scripts/audit_skill_overlap.ts +233 -95
- package/src/scripts/audit_skill_overlap_allowlist.json +4 -0
- package/src/scripts/build_proof.ts +15 -1
- package/src/scripts/check_artefact_count_messaging.ts +128 -15
- package/src/scripts/check_dependency_floors.ts +16 -6
- package/src/scripts/check_gate_coverage.ts +6 -6
- package/src/scripts/check_kernel_rule_bundle.ts +5 -4
- package/src/scripts/check_token_quality_golden.ts +0 -18
- package/src/scripts/code_graph/loader.ts +35 -6
- package/src/scripts/compile_router.ts +3 -8
- package/src/scripts/condense.ts +2 -2
- package/src/scripts/count_scoped_projection.ts +104 -0
- package/src/scripts/explain_run.ts +0 -1
- package/src/scripts/generate_pack_manifests.ts +3 -20
- package/src/scripts/hooks/block_kernel_rule_writes.ts +145 -10
- package/src/scripts/hooks/block_no_verify.ts +101 -0
- package/src/scripts/install.ts +13 -90
- package/src/scripts/lint_agent_security.ts +5 -4
- package/src/scripts/lint_framework_leakage_allowlist.json +2 -2
- package/src/scripts/lint_regression.ts +92 -1
- package/src/scripts/lint_skill_descriptions.ts +93 -5
- package/src/scripts/project_thin_rules.ts +2 -2
- package/src/scripts/release.ts +107 -3
- package/src/scripts/resolve_lint_scope.ts +53 -0
- package/src/scripts/router_telemetry.ts +0 -5
- package/src/scripts/runtime_dispatcher.ts +40 -0
- package/src/scripts/schemas/pack.schema.json +1 -5
- package/src/scripts/schemas/rule.schema.json +1 -2
- package/src/scripts/self_review_gate.ts +15 -73
- package/src/scripts/skill_linter.ts +160 -11
- package/src/scripts/sweep_dead_scan_roots.ts +550 -0
- package/src/scripts/trigger_coverage.ts +11 -37
- package/src/scripts/update_counts.ts +90 -11
|
@@ -0,0 +1,550 @@
|
|
|
1
|
+
#!/usr/bin/env -S node --import tsx
|
|
2
|
+
/**
|
|
3
|
+
* sweep_dead_scan_roots — which gate scripts walk a root that does not exist?
|
|
4
|
+
*
|
|
5
|
+
* Population expansion for `road-to-gates-that-can-fail` Phase 1. The committed
|
|
6
|
+
* census (`agents/evidence/reports/gate-scope-census.md`) covers the 14 gates a
|
|
7
|
+
* manual audit confirmed dead; extending it to the full population is that
|
|
8
|
+
* step's stated remaining work. This is the deterministic instrument for it.
|
|
9
|
+
*
|
|
10
|
+
* WHY IT EXISTS: two more dead gates were found OUTSIDE the censused 14 while
|
|
11
|
+
* doing thematically unrelated work — `audit_skill_overlap` (rooted at a
|
|
12
|
+
* deleted container, 0 of 287 skills read) and `lint_media_policy_linkage`
|
|
13
|
+
* (`agents/policies/media` absent, exits 0 with "nothing to lint"). Two
|
|
14
|
+
* incidental finds outside an audited set is a base-rate question, not an
|
|
15
|
+
* anecdote, and a hand-written census cannot answer it repeatably.
|
|
16
|
+
*
|
|
17
|
+
* ── CRITERION ──────────────────────────────────────────────────────────────
|
|
18
|
+
*
|
|
19
|
+
* A finding is a repo path assembled from string literals over a ROOT-like
|
|
20
|
+
* base that does not exist on disk. It is CONFIRMED only with POSITIVE READ
|
|
21
|
+
* EVIDENCE — the path must demonstrably be read, not merely mentioned:
|
|
22
|
+
*
|
|
23
|
+
* direct-read fs read called with the identifier
|
|
24
|
+
* helper-read:<fn> passed to a same-file function whose body reads
|
|
25
|
+
* inline-read a read wraps the join expression itself
|
|
26
|
+
* derived-read-><id>:<ev> a derivation chain that TERMINATES in a read
|
|
27
|
+
* array-iterated-*:<arr> member of a root array whose loop variable reads
|
|
28
|
+
* spec-iterated-*:<arr> property of an iterated spec table, prop reads
|
|
29
|
+
*
|
|
30
|
+
* "Not written to" is deliberately NOT evidence, and neither is derivation on
|
|
31
|
+
* its own. Both were tried and both produced false positives — see the
|
|
32
|
+
* evidence log below. Everything else missing-but-unproven is reported as
|
|
33
|
+
* UNPROVEN, never dropped: static single-file analysis cannot see reads that
|
|
34
|
+
* cross a module boundary, so the confirmed set is a floor, not a census.
|
|
35
|
+
*
|
|
36
|
+
* ── EVIDENCE LOG (every rule below was added by a measured failure) ────────
|
|
37
|
+
*
|
|
38
|
+
* 1. Reads inside function bodies and lowercase locals count. A first cut
|
|
39
|
+
* read only top-level uppercase constants and MISSED `audit_skill_overlap`
|
|
40
|
+
* — a sweep that misses a known-dead gate is itself a dead gate.
|
|
41
|
+
* 2. Positive read evidence, not absence of writes. "Not write-only" flagged
|
|
42
|
+
* `check_gate_paths`, whose `packages` entry is a membership allowlist for
|
|
43
|
+
* an `_is_under_source_tree` predicate. Path-as-predicate is not
|
|
44
|
+
* path-as-root.
|
|
45
|
+
* 3. Array membership, with the loop variable required to read. Multi-root
|
|
46
|
+
* gates collect roots in an array literal, leaving no per-root identifier
|
|
47
|
+
* — that is why `lint_media_policy_linkage` reported 1 of its 4 dead roots.
|
|
48
|
+
* Treating iteration alone as evidence re-flagged `check_gate_paths`
|
|
49
|
+
* (it iterates its allowlist into `startsWith`), so the loop variable must
|
|
50
|
+
* itself flow into a read.
|
|
51
|
+
* 4. Derivation must terminate in a read. `derived-join` used to assert only
|
|
52
|
+
* "this identifier is the base of a further join" — derivation, not
|
|
53
|
+
* lecture. It carried four class-A findings.
|
|
54
|
+
* 5. Property joins in iterated spec tables. Rule 4 demoted `lint_namespace`,
|
|
55
|
+
* which the census independently lists as dead: its roots live as object
|
|
56
|
+
* properties in a `TARGETS` table whose destructured loop variable feeds a
|
|
57
|
+
* reading helper. Neither the identifier trail nor the array tracer could
|
|
58
|
+
* attach.
|
|
59
|
+
*
|
|
60
|
+
* Not built, on purpose: `forEach`/`map` spec variants and property joins
|
|
61
|
+
* outside array literals. Neither occurs in this corpus. A rule without a
|
|
62
|
+
* measured occurrence is the speculative extension this log exists to prevent.
|
|
63
|
+
*
|
|
64
|
+
* ── DISPOSITIONS LEDGER ────────────────────────────────────────────────────
|
|
65
|
+
*
|
|
66
|
+
* Findings cleared by manual review live in `agents/evidence/sweep-dispositions.json`
|
|
67
|
+
* ({script, rel, category, reason, date}). They are PRINTED as
|
|
68
|
+
* `known-benign (<category>)`, never silently suppressed — the sweep has
|
|
69
|
+
* memory so the same three cases are not re-litigated every run. A ledger
|
|
70
|
+
* entry with no matching finding is STALE (the code moved; re-review), and the
|
|
71
|
+
* ledger is capped so growth forces re-litigation instead of quiet accretion.
|
|
72
|
+
*
|
|
73
|
+
* ── EXIT CONTRACT (one meaning per code, so red stays informative) ─────────
|
|
74
|
+
*
|
|
75
|
+
* 0 clean
|
|
76
|
+
* 1 confirmed class-A finding(s) — real dead roots. Nothing else returns 1.
|
|
77
|
+
* 2 self-test failure — the extractor is blind; output is not trustworthy.
|
|
78
|
+
* 3 ledger hygiene — stale entry or over cap. The sweep ran fine; the LIST
|
|
79
|
+
* needs work.
|
|
80
|
+
*
|
|
81
|
+
* Real findings outrank hygiene: a stale entry alongside genuine class-A hits
|
|
82
|
+
* still exits 1, with the stale warning on stderr. Returning 3 there would let
|
|
83
|
+
* a bookkeeping problem mask dead gates — the precise failure this gate exists
|
|
84
|
+
* to catch, reproduced in the gate itself.
|
|
85
|
+
*
|
|
86
|
+
* Usage:
|
|
87
|
+
* ./scripts-run src/scripts/sweep_dead_scan_roots [--quiet] [--json <path>]
|
|
88
|
+
* [--root <dir>] [--ledger <path>]
|
|
89
|
+
*/
|
|
90
|
+
|
|
91
|
+
import * as fs from 'node:fs';
|
|
92
|
+
import * as path from 'node:path';
|
|
93
|
+
import process from 'node:process';
|
|
94
|
+
import { fileURLToPath } from 'node:url';
|
|
95
|
+
|
|
96
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
97
|
+
const REPO = path.resolve(path.dirname(_HERE), '..', '..');
|
|
98
|
+
const DEFAULT_SCRIPTS = path.join(REPO, 'src', 'scripts');
|
|
99
|
+
const DEFAULT_LEDGER = path.join(REPO, 'agents', 'evidence', 'sweep-dispositions.json');
|
|
100
|
+
const LEDGER_CAP = 15;
|
|
101
|
+
|
|
102
|
+
/** Population: the gate-name prefix convention the census uses. */
|
|
103
|
+
const GATE_RE = /^(lint|audit|check|verify)_.*\.ts$/;
|
|
104
|
+
|
|
105
|
+
/** Identifiers accepted as a repo-root base for a literal join. */
|
|
106
|
+
const BASE_IDS: ReadonlySet<string> = new Set([
|
|
107
|
+
'ROOT', 'REPO_ROOT', 'REPO', 'PROJECT_ROOT', 'REPO_DIR',
|
|
108
|
+
]);
|
|
109
|
+
|
|
110
|
+
const READ_FNS = 'readdirSync|readFileSync|statSync|opendirSync|existsSync|globSync';
|
|
111
|
+
const READ_RE = new RegExp(`(${READ_FNS})`);
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Triage classes. Assignment is mechanical; disposition is a human call.
|
|
115
|
+
* A — pre-ADR-051 containers: the exact class the census repaired 14 of.
|
|
116
|
+
* B — build artifacts consumed as inputs: missing in a fresh clone is only a
|
|
117
|
+
* defect if CI runs the gate before its producer.
|
|
118
|
+
* C — everything else: optional surface (→ `allowEmpty` with a reason) or a
|
|
119
|
+
* retired feature (→ retire the gate).
|
|
120
|
+
*/
|
|
121
|
+
const CLASS_A_RETIRED: readonly string[] = ['.agent-src.uncondensed', 'packages'];
|
|
122
|
+
const CLASS_B_ARTIFACT: readonly string[] = ['dist/', 'internal/bench/reports/', '.github/', 'site/dist'];
|
|
123
|
+
|
|
124
|
+
export type TriageClass = 'A' | 'B' | 'C';
|
|
125
|
+
|
|
126
|
+
export function classify(rel: string): TriageClass {
|
|
127
|
+
if (CLASS_A_RETIRED.some((p) => rel === p || rel.startsWith(`${p}/`))) return 'A';
|
|
128
|
+
if (CLASS_B_ARTIFACT.some((p) => rel.startsWith(p))) return 'B';
|
|
129
|
+
return 'C';
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export interface Finding {
|
|
133
|
+
rel: string;
|
|
134
|
+
names: string;
|
|
135
|
+
evidence: string;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export interface Analysis {
|
|
139
|
+
confirmed: Finding[];
|
|
140
|
+
unproven: Finding[];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export interface Disposition {
|
|
144
|
+
script: string;
|
|
145
|
+
rel: string;
|
|
146
|
+
category: string;
|
|
147
|
+
reason: string;
|
|
148
|
+
date: string;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Literal segments of a `path.join(base, 'a', 'b')` tail, joined POSIX-style. */
|
|
152
|
+
function literals(tail: string): string {
|
|
153
|
+
return [...tail.matchAll(/'([^']*)'/g)].map((x) => x[1]).join('/');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Same-file functions whose body contains a read (crude slice, deliberately). */
|
|
157
|
+
function localReaders(src: string): Set<string> {
|
|
158
|
+
const out = new Set<string>();
|
|
159
|
+
const re =
|
|
160
|
+
/(?:function\s+([A-Za-z_$][\w$]*)|const\s+([A-Za-z_$][\w$]*)\s*=\s*(?:\([^)]*\)|[A-Za-z_$][\w$]*)\s*=>)/g;
|
|
161
|
+
let m: RegExpExecArray | null;
|
|
162
|
+
while ((m = re.exec(src)) !== null) {
|
|
163
|
+
const name = (m[1] ?? m[2]) as string;
|
|
164
|
+
if (READ_RE.test(src.slice(m.index, m.index + 3000))) out.add(name);
|
|
165
|
+
}
|
|
166
|
+
return out;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Does `v` flow into a read within `scope`? Returns the evidence kind. */
|
|
170
|
+
function readsWithin(src: string, scope: string, v: string, readers: Set<string>, kind: string): string | null {
|
|
171
|
+
if (new RegExp(`(${READ_FNS})\\([^)]*\\b${v}\\b`).test(scope)) return `${kind}-read`;
|
|
172
|
+
for (const r of readers) {
|
|
173
|
+
if (new RegExp(`\\b${r}\\(\\s*${v}\\b`).test(scope)) return `${kind}-helper:${r}`;
|
|
174
|
+
}
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** `const NAME = [ path.join(<BASE>, 'lit'…), … ]` — roots collected in an array. */
|
|
179
|
+
function arrayRootDecls(src: string): Array<{ name: string; rels: string[] }> {
|
|
180
|
+
const out: Array<{ name: string; rels: string[] }> = [];
|
|
181
|
+
const re = /const\s+([A-Za-z_$][\w$]*)\s*(?::[^=]+)?=\s*\[([\s\S]*?)\]/g;
|
|
182
|
+
let m: RegExpExecArray | null;
|
|
183
|
+
while ((m = re.exec(src)) !== null) {
|
|
184
|
+
const rels: string[] = [];
|
|
185
|
+
const jre = /path\.join\(\s*([A-Za-z_$][\w$]*)\s*((?:,\s*'[^']*')+)\s*\)/g;
|
|
186
|
+
let j: RegExpExecArray | null;
|
|
187
|
+
while ((j = jre.exec(m[2] as string)) !== null) {
|
|
188
|
+
if (BASE_IDS.has(j[1] as string)) rels.push(literals(j[2] as string));
|
|
189
|
+
}
|
|
190
|
+
if (rels.length) out.push({ name: m[1] as string, rels });
|
|
191
|
+
}
|
|
192
|
+
return out;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Read-shaped use of a whole root array — inherited by every member. */
|
|
196
|
+
function arrayEvidence(src: string, name: string, readers: Set<string>): string | null {
|
|
197
|
+
const forRe = new RegExp(`for\\s*\\(\\s*const\\s+([\\w$]+)\\s+of\\s+${name}\\b`, 'g');
|
|
198
|
+
let m: RegExpExecArray | null;
|
|
199
|
+
while ((m = forRe.exec(src)) !== null) {
|
|
200
|
+
const ev = readsWithin(src, src.slice(m.index, m.index + 400), m[1] as string, readers, 'array-iterated');
|
|
201
|
+
if (ev) return ev;
|
|
202
|
+
}
|
|
203
|
+
const cbRe = new RegExp(`${name}\\.(?:forEach|map|flatMap|filter)\\(\\s*\\(?([\\w$]+)\\)?\\s*=>`, 'g');
|
|
204
|
+
while ((m = cbRe.exec(src)) !== null) {
|
|
205
|
+
const ev = readsWithin(src, src.slice(m.index, m.index + 400), m[1] as string, readers, 'array-traversed');
|
|
206
|
+
if (ev) return ev;
|
|
207
|
+
}
|
|
208
|
+
for (const r of readers) {
|
|
209
|
+
if (new RegExp(`\\b${r}\\(\\s*(?:\\.\\.\\.)?${name}\\b`).test(src)) return `array-passed-helper:${r}`;
|
|
210
|
+
}
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Identifier → repo-relative path, two hops, so property joins over a derived parent resolve. */
|
|
215
|
+
function resolveMap(src: string): Map<string, string> {
|
|
216
|
+
const out = new Map<string, string>();
|
|
217
|
+
const re =
|
|
218
|
+
/(?:const|let)\s+([A-Za-z_$][\w$]*)\s*=\s*path\.join\(\s*([A-Za-z_$][\w$]*)\s*((?:,\s*'[^']*')*)\s*\)/g;
|
|
219
|
+
for (let hop = 0; hop < 2; hop += 1) {
|
|
220
|
+
re.lastIndex = 0;
|
|
221
|
+
let m: RegExpExecArray | null;
|
|
222
|
+
while ((m = re.exec(src)) !== null) {
|
|
223
|
+
const [, name, base, tail] = m;
|
|
224
|
+
const lit = literals(tail as string);
|
|
225
|
+
if (BASE_IDS.has(base as string)) {
|
|
226
|
+
if (lit) out.set(name as string, lit);
|
|
227
|
+
} else if (out.has(base as string)) {
|
|
228
|
+
const parent = out.get(base as string) as string;
|
|
229
|
+
out.set(name as string, lit ? `${parent}/${lit}` : parent);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return out;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function analyze(src: string, repoRoot: string): Analysis {
|
|
237
|
+
const readers = localReaders(src);
|
|
238
|
+
const resolved = resolveMap(src);
|
|
239
|
+
|
|
240
|
+
// Property joins inside array-literal spec tables; evidence comes from the
|
|
241
|
+
// DESTRUCTURED property flowing into a read.
|
|
242
|
+
const specHits = new Map<string, string>();
|
|
243
|
+
const specUnproven = new Set<string>();
|
|
244
|
+
{
|
|
245
|
+
const are = /const\s+([A-Za-z_$][\w$]*)\s*(?::[^=]+)?=\s*\[([\s\S]*?)\];/g;
|
|
246
|
+
let am: RegExpExecArray | null;
|
|
247
|
+
while ((am = are.exec(src)) !== null) {
|
|
248
|
+
const [, arrName, body] = am;
|
|
249
|
+
const pre =
|
|
250
|
+
/([A-Za-z_$][\w$]*)\s*:\s*path\.join\(\s*([A-Za-z_$][\w$]*)\s*((?:,\s*'[^']*')*)\s*\)/g;
|
|
251
|
+
const propRels: Array<{ prop: string; rel: string }> = [];
|
|
252
|
+
let pm: RegExpExecArray | null;
|
|
253
|
+
while ((pm = pre.exec(body as string)) !== null) {
|
|
254
|
+
const [, prop, base, tail] = pm;
|
|
255
|
+
const lit = literals(tail as string);
|
|
256
|
+
let rel: string | null = null;
|
|
257
|
+
if (BASE_IDS.has(base as string)) rel = lit || null;
|
|
258
|
+
else if (resolved.has(base as string)) {
|
|
259
|
+
const parent = resolved.get(base as string) as string;
|
|
260
|
+
rel = lit ? `${parent}/${lit}` : parent;
|
|
261
|
+
}
|
|
262
|
+
if (rel) propRels.push({ prop: prop as string, rel });
|
|
263
|
+
}
|
|
264
|
+
if (!propRels.length) continue;
|
|
265
|
+
|
|
266
|
+
const evByProp = new Map<string, string>();
|
|
267
|
+
const fre = new RegExp(`for\\s*\\(\\s*const\\s*\\{([^}]*)\\}\\s+of\\s+${arrName}\\b`, 'g');
|
|
268
|
+
let fm: RegExpExecArray | null;
|
|
269
|
+
while ((fm = fre.exec(src)) !== null) {
|
|
270
|
+
const scope = src.slice(fm.index, fm.index + 400);
|
|
271
|
+
for (const raw of (fm[1] as string).split(',')) {
|
|
272
|
+
const v = raw.trim().split(':').pop()?.trim() ?? '';
|
|
273
|
+
if (!v || evByProp.has(v)) continue;
|
|
274
|
+
const ev = readsWithin(src, scope, v, readers, 'spec-iterated');
|
|
275
|
+
if (ev) evByProp.set(v, ev);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
for (const { prop, rel } of propRels) {
|
|
279
|
+
const ev = evByProp.get(prop);
|
|
280
|
+
if (ev) specHits.set(rel, `${ev}:${arrName}`);
|
|
281
|
+
else specUnproven.add(rel);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const arrayHits = new Map<string, string>();
|
|
287
|
+
const arrayUnproven = new Set<string>();
|
|
288
|
+
for (const arr of arrayRootDecls(src)) {
|
|
289
|
+
const ev = arrayEvidence(src, arr.name, readers);
|
|
290
|
+
for (const rel of arr.rels) {
|
|
291
|
+
if (ev) arrayHits.set(rel, `${ev}:${arr.name}`);
|
|
292
|
+
else arrayUnproven.add(rel);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Direct literal joins over a base, with their declaring identifier.
|
|
297
|
+
const found = new Map<string, { names: Set<string>; inlineRead: boolean }>();
|
|
298
|
+
{
|
|
299
|
+
const re =
|
|
300
|
+
/(?:(?:const|let)\s+([A-Za-z_$][\w$]*)\s*=\s*)?path\.join\(\s*([A-Za-z_$][\w$]*)\s*((?:,\s*'[^']*')+)\s*\)/g;
|
|
301
|
+
let m: RegExpExecArray | null;
|
|
302
|
+
while ((m = re.exec(src)) !== null) {
|
|
303
|
+
const [, name, base, tail] = m;
|
|
304
|
+
if (!BASE_IDS.has(base as string)) continue;
|
|
305
|
+
const rel = literals(tail as string);
|
|
306
|
+
if (!rel) continue;
|
|
307
|
+
const e = found.get(rel) ?? { names: new Set<string>(), inlineRead: false };
|
|
308
|
+
if (name) e.names.add(name);
|
|
309
|
+
// Anchored on purpose: the read must WRAP the join. An unanchored
|
|
310
|
+
// lookback bled across newlines and handed a preceding statement's
|
|
311
|
+
// read to an unrelated array member.
|
|
312
|
+
else if (new RegExp(`(${READ_FNS})\\s*\\($`).test(src.slice(Math.max(0, m.index - 40), m.index))) {
|
|
313
|
+
e.inlineRead = true;
|
|
314
|
+
}
|
|
315
|
+
found.set(rel, e);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Derivation graph: parent identifier → children declared from it.
|
|
320
|
+
const derived = new Map<string, string[]>();
|
|
321
|
+
{
|
|
322
|
+
const re =
|
|
323
|
+
/(?:const|let)\s+([A-Za-z_$][\w$]*)\s*=\s*path\.join\(\s*([A-Za-z_$][\w$]*)\s*(?:,\s*'[^']*')*\s*\)/g;
|
|
324
|
+
let m: RegExpExecArray | null;
|
|
325
|
+
while ((m = re.exec(src)) !== null) {
|
|
326
|
+
const [, child, parent] = m;
|
|
327
|
+
if (BASE_IDS.has(parent as string)) continue;
|
|
328
|
+
derived.set(parent as string, [...(derived.get(parent as string) ?? []), child as string]);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const identifierEvidence = (n: string, seen: Set<string>, depth: number): string | null => {
|
|
333
|
+
if (depth > 3 || seen.has(n)) return null;
|
|
334
|
+
seen.add(n);
|
|
335
|
+
if (new RegExp(`(${READ_FNS})\\([^)]*\\b${n}\\b`).test(src)) return 'direct-read';
|
|
336
|
+
for (const r of readers) {
|
|
337
|
+
if (new RegExp(`\\b${r}\\(\\s*${n}\\b`).test(src)) return `helper-read:${r}`;
|
|
338
|
+
}
|
|
339
|
+
for (const child of derived.get(n) ?? []) {
|
|
340
|
+
const ev = identifierEvidence(child, seen, depth + 1);
|
|
341
|
+
if (ev) return `derived-read->${child}:${ev}`;
|
|
342
|
+
}
|
|
343
|
+
return null; // derivation without a terminating read is NOT evidence
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
const confirmed: Finding[] = [];
|
|
347
|
+
const unproven: Finding[] = [];
|
|
348
|
+
const missing = (rel: string): boolean => !fs.existsSync(path.join(repoRoot, rel));
|
|
349
|
+
|
|
350
|
+
for (const [rel, e] of found) {
|
|
351
|
+
if (!missing(rel)) continue;
|
|
352
|
+
let evidence: string | null =
|
|
353
|
+
e.inlineRead ? 'inline-read' : (arrayHits.get(rel) ?? specHits.get(rel) ?? null);
|
|
354
|
+
let writeOnly = false;
|
|
355
|
+
for (const n of e.names) {
|
|
356
|
+
if (evidence) break;
|
|
357
|
+
evidence = identifierEvidence(n, new Set<string>(), 0);
|
|
358
|
+
if (!evidence) {
|
|
359
|
+
writeOnly = new RegExp(
|
|
360
|
+
`(mkdirSync|writeFileSync|appendFileSync|copyFileSync)\\([^)]*\\b${n}\\b`,
|
|
361
|
+
).test(src);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
const hit: Finding = { rel, names: [...e.names].join(',') || '(inline)', evidence: evidence ?? '—' };
|
|
365
|
+
if (evidence) confirmed.push(hit);
|
|
366
|
+
else if (!writeOnly) unproven.push(hit);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const push = (target: Finding[], other: Finding[], rel: string, names: string, evidence: string): void => {
|
|
370
|
+
if (!missing(rel)) return;
|
|
371
|
+
if (confirmed.some((h) => h.rel === rel) || other.some((h) => h.rel === rel)) return;
|
|
372
|
+
target.push({ rel, names, evidence });
|
|
373
|
+
};
|
|
374
|
+
for (const [rel, ev] of specHits) push(confirmed, [], rel, '(spec-table)', ev);
|
|
375
|
+
for (const rel of specUnproven) push(unproven, unproven, rel, '(spec-table)', '—');
|
|
376
|
+
for (const [rel, ev] of arrayHits) push(confirmed, [], rel, '(array)', ev);
|
|
377
|
+
for (const rel of arrayUnproven) push(unproven, unproven, rel, '(array)', '—');
|
|
378
|
+
|
|
379
|
+
return { confirmed, unproven };
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* In-process guard: the dual fixture — the one case that pins BOTH directions
|
|
384
|
+
* of the precision/recall trade. A spec-table root (the `lint_namespace` shape)
|
|
385
|
+
* must be confirmed; a predicate-only iteration (the `check_gate_paths` shape)
|
|
386
|
+
* must not. Every other fixture lives in the test file; this one runs in
|
|
387
|
+
* production because a blind extractor must refuse to report at all.
|
|
388
|
+
*/
|
|
389
|
+
export function selfTest(repoRoot: string): boolean {
|
|
390
|
+
const fixture = [
|
|
391
|
+
"const SRC_J = path.join(ROOT, 'agents', 'sweep-selftest-absent-j');",
|
|
392
|
+
"const TARGETS_J = [{ kind: 'skill', root: path.join(SRC_J, 'skills'), glob: '*.md' }];",
|
|
393
|
+
'function _globJ(p, g) { return fs.readdirSync(p).filter((x) => x.endsWith(g)); }',
|
|
394
|
+
'for (const { kind, root, glob } of TARGETS_J) { _globJ(root, glob); }',
|
|
395
|
+
"const ALLOW_K = [{ label: 'x', root: path.join(ROOT, 'agents', 'sweep-selftest-absent-k') }];",
|
|
396
|
+
'for (const { label, root } of ALLOW_K) { if (target.startsWith(root)) { ok = true; } }',
|
|
397
|
+
].join('\n');
|
|
398
|
+
const { confirmed, unproven } = analyze(fixture, repoRoot);
|
|
399
|
+
const c = new Set(confirmed.map((h) => h.rel));
|
|
400
|
+
const u = new Set(unproven.map((h) => h.rel));
|
|
401
|
+
return (
|
|
402
|
+
c.has('agents/sweep-selftest-absent-j/skills') &&
|
|
403
|
+
!c.has('agents/sweep-selftest-absent-k') &&
|
|
404
|
+
u.has('agents/sweep-selftest-absent-k')
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export function loadLedger(file: string): Disposition[] {
|
|
409
|
+
if (!fs.existsSync(file)) return [];
|
|
410
|
+
const data = JSON.parse(fs.readFileSync(file, 'utf-8')) as Disposition[];
|
|
411
|
+
return Array.isArray(data) ? data : [];
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
interface Args {
|
|
415
|
+
quiet: boolean;
|
|
416
|
+
json: string | null;
|
|
417
|
+
scripts: string;
|
|
418
|
+
ledger: string;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
export function parseArgs(argv: string[]): Args {
|
|
422
|
+
const a: Args = { quiet: false, json: null, scripts: DEFAULT_SCRIPTS, ledger: DEFAULT_LEDGER };
|
|
423
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
424
|
+
const arg = argv[i];
|
|
425
|
+
if (arg === '--quiet') a.quiet = true;
|
|
426
|
+
else if (arg === '--json') a.json = argv[(i += 1)] ?? '';
|
|
427
|
+
else if (arg === '--root') a.scripts = path.resolve(REPO, argv[(i += 1)] ?? DEFAULT_SCRIPTS);
|
|
428
|
+
else if (arg === '--ledger') a.ledger = path.resolve(REPO, argv[(i += 1)] ?? DEFAULT_LEDGER);
|
|
429
|
+
else {
|
|
430
|
+
process.stderr.write(`sweep_dead_scan_roots: unrecognized argument: ${arg}\n`);
|
|
431
|
+
process.exit(2);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return a;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
export function main(argv: string[]): number {
|
|
438
|
+
const args = parseArgs(argv);
|
|
439
|
+
const repoRoot = path.resolve(args.scripts, '..', '..');
|
|
440
|
+
|
|
441
|
+
if (!selfTest(repoRoot)) {
|
|
442
|
+
process.stderr.write(
|
|
443
|
+
'sweep_dead_scan_roots: SELF-TEST FAILED — the extractor is blind on a pinned mode, refusing to report.\n',
|
|
444
|
+
);
|
|
445
|
+
return 2;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
let ledger: Disposition[];
|
|
449
|
+
try {
|
|
450
|
+
ledger = loadLedger(args.ledger);
|
|
451
|
+
} catch (exc) {
|
|
452
|
+
process.stderr.write(`sweep_dead_scan_roots: unreadable ledger — ${String(exc)}\n`);
|
|
453
|
+
return 3;
|
|
454
|
+
}
|
|
455
|
+
if (ledger.length > LEDGER_CAP) {
|
|
456
|
+
process.stderr.write(
|
|
457
|
+
`sweep_dead_scan_roots: ledger has ${ledger.length} entries (cap ${LEDGER_CAP}) — ` +
|
|
458
|
+
're-litigate the dispositions before adding more.\n',
|
|
459
|
+
);
|
|
460
|
+
return 3;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const pop = fs.existsSync(args.scripts)
|
|
464
|
+
? fs.readdirSync(args.scripts).filter((f) => GATE_RE.test(f)).sort()
|
|
465
|
+
: [];
|
|
466
|
+
const results = new Map<string, Analysis>();
|
|
467
|
+
for (const f of pop) {
|
|
468
|
+
const r = analyze(fs.readFileSync(path.join(args.scripts, f), 'utf-8'), repoRoot);
|
|
469
|
+
if (r.confirmed.length || r.unproven.length) results.set(f, r);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const used = new Set<Disposition>();
|
|
473
|
+
const disposed = (script: string, rel: string): Disposition | undefined => {
|
|
474
|
+
const hit = ledger.find((l) => l.script === script && l.rel === rel);
|
|
475
|
+
if (hit) used.add(hit);
|
|
476
|
+
return hit;
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
let classA = 0;
|
|
480
|
+
const lines: string[] = [];
|
|
481
|
+
for (const [script, r] of results) {
|
|
482
|
+
for (const h of r.confirmed) {
|
|
483
|
+
const d = disposed(script, h.rel);
|
|
484
|
+
if (d) {
|
|
485
|
+
lines.push(` CONFIRMED ${script}: ${h.rel} — known-benign (${d.category}): ${d.reason}`);
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
const cls = classify(h.rel);
|
|
489
|
+
if (cls === 'A') classA += 1;
|
|
490
|
+
lines.push(` CONFIRMED [${cls}] ${script}: ${h.rel} [${h.evidence}] (${h.names})`);
|
|
491
|
+
}
|
|
492
|
+
for (const h of r.unproven) {
|
|
493
|
+
const d = disposed(script, h.rel);
|
|
494
|
+
lines.push(
|
|
495
|
+
d
|
|
496
|
+
? ` unproven ${script}: ${h.rel} — known-benign (${d.category}): ${d.reason}`
|
|
497
|
+
: ` unproven [${classify(h.rel)}] ${script}: ${h.rel} (${h.names})`,
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const stale = ledger.filter((l) => !used.has(l));
|
|
503
|
+
const confirmedCount = [...results.values()].reduce((n, r) => n + r.confirmed.length, 0);
|
|
504
|
+
const unprovenCount = [...results.values()].reduce((n, r) => n + r.unproven.length, 0);
|
|
505
|
+
|
|
506
|
+
if (args.json) {
|
|
507
|
+
fs.mkdirSync(path.dirname(args.json), { recursive: true });
|
|
508
|
+
fs.writeFileSync(
|
|
509
|
+
args.json,
|
|
510
|
+
`${JSON.stringify(
|
|
511
|
+
{
|
|
512
|
+
population: pop.length,
|
|
513
|
+
confirmed: confirmedCount,
|
|
514
|
+
unproven: unprovenCount,
|
|
515
|
+
class_a: classA,
|
|
516
|
+
stale: stale.map((s) => `${s.script}:${s.rel}`),
|
|
517
|
+
findings: [...results].map(([script, r]) => ({ script, ...r })),
|
|
518
|
+
},
|
|
519
|
+
null,
|
|
520
|
+
2,
|
|
521
|
+
)}\n`,
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
if (!args.quiet) {
|
|
526
|
+
process.stdout.write(`sweep_dead_scan_roots: ${pop.length} gate script(s) scanned\n`);
|
|
527
|
+
for (const l of lines) process.stdout.write(`${l}\n`);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
if (stale.length) {
|
|
531
|
+
process.stderr.write(
|
|
532
|
+
`⚠️ STALE ledger entries (no matching finding — the code moved, re-review): ` +
|
|
533
|
+
`${stale.map((l) => `${l.script}:${l.rel}`).join(', ')}\n`,
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Real findings outrank ledger hygiene — never let bookkeeping mask a dead gate.
|
|
538
|
+
const exit = classA > 0 ? 1 : stale.length > 0 ? 3 : 0;
|
|
539
|
+
if (!args.quiet || exit !== 0) {
|
|
540
|
+
process.stdout.write(
|
|
541
|
+
`sweep_dead_scan_roots: ${confirmedCount} confirmed, ${unprovenCount} unproven, ` +
|
|
542
|
+
`${classA} class-A, ${stale.length} stale → exit ${exit}\n`,
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
return exit;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
if (path.resolve(process.argv[1] ?? '') === path.resolve(_HERE)) {
|
|
549
|
+
process.exit(main(process.argv.slice(2)));
|
|
550
|
+
}
|
|
@@ -19,8 +19,14 @@
|
|
|
19
19
|
* - kernel rules always fire (always-on layer).
|
|
20
20
|
* - a tier rule fires iff any of its triggers matches the prompt:
|
|
21
21
|
* - `keyword` → case-insensitive substring.
|
|
22
|
-
* - `
|
|
23
|
-
*
|
|
22
|
+
* - `phrase` → case-insensitive substring (multi-word).
|
|
23
|
+
*
|
|
24
|
+
* `intent` used to be the second matcher here, on word-set-inclusion
|
|
25
|
+
* semantics, while `router_telemetry` documented the same key as
|
|
26
|
+
* "informational only — never auto-matches". That divergence is gone: the
|
|
27
|
+
* trigger type was removed, and `phrase` — which every other tool already
|
|
28
|
+
* treats as a real matcher — took its place. A rule now proves it can fire on
|
|
29
|
+
* a substring a prompt actually contains, not on a bag of words.
|
|
24
30
|
*
|
|
25
31
|
* A case fails when an expected rule is NOT in the fired set. Exit 1 on any
|
|
26
32
|
* miss → the merge that would have shrunk the rule is blocked (2.2).
|
|
@@ -40,9 +46,6 @@ export const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
|
40
46
|
export const ROUTER = path.join(REPO_ROOT, 'dist', 'router.json');
|
|
41
47
|
export const CORPUS = path.join(REPO_ROOT, 'tests', 'eval', 'trigger-coverage.yaml');
|
|
42
48
|
|
|
43
|
-
// _WORD = re.compile(r"[a-z][a-z0-9_]+") — matched against text.lower().
|
|
44
|
-
const _WORD = /[a-z][a-z0-9_]+/g;
|
|
45
|
-
|
|
46
49
|
type Router = {
|
|
47
50
|
kernel?: string[];
|
|
48
51
|
tier_1?: Array<{ id: string; triggers?: Array<Record<string, string>> }>;
|
|
@@ -63,48 +66,19 @@ interface CaseResult {
|
|
|
63
66
|
expect: string[];
|
|
64
67
|
}
|
|
65
68
|
|
|
66
|
-
function _tokens(text: string): Set<string> {
|
|
67
|
-
const out = new Set<string>();
|
|
68
|
-
const low = text.toLowerCase();
|
|
69
|
-
_WORD.lastIndex = 0;
|
|
70
|
-
let m: RegExpExecArray | null;
|
|
71
|
-
while ((m = _WORD.exec(low)) !== null) {
|
|
72
|
-
const w = m[0];
|
|
73
|
-
if (w.length > 2) {
|
|
74
|
-
out.add(w);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
return out;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function _isSubset(small: Set<string>, big: Set<string>): boolean {
|
|
81
|
-
for (const v of small) {
|
|
82
|
-
if (!big.has(v)) {
|
|
83
|
-
return false;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
return true;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
69
|
export function load_router(): Router {
|
|
90
70
|
return JSON.parse(fs.readFileSync(ROUTER, 'utf-8')) as Router;
|
|
91
71
|
}
|
|
92
72
|
|
|
93
73
|
export function fired_rules(prompt: string, router: Router): Set<string> {
|
|
94
74
|
const low = prompt.toLowerCase();
|
|
95
|
-
const toks = _tokens(prompt);
|
|
96
75
|
const fired = new Set<string>(router.kernel ?? []);
|
|
97
76
|
for (const tier of ['tier_1', 'tier_2'] as const) {
|
|
98
77
|
for (const entry of router[tier] ?? []) {
|
|
99
78
|
for (const trig of entry.triggers ?? []) {
|
|
100
|
-
if ('keyword' in trig) {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
break;
|
|
104
|
-
}
|
|
105
|
-
} else if ('intent' in trig) {
|
|
106
|
-
const words = _tokens(trig['intent'] as string);
|
|
107
|
-
if (words.size > 0 && _isSubset(words, toks)) {
|
|
79
|
+
if ('keyword' in trig || 'phrase' in trig) {
|
|
80
|
+
const needle = String(trig['keyword'] ?? trig['phrase']).toLowerCase();
|
|
81
|
+
if (needle !== '' && low.includes(needle)) {
|
|
108
82
|
fired.add(entry.id);
|
|
109
83
|
break;
|
|
110
84
|
}
|