@event4u/agent-config 9.12.0 → 9.13.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 +80 -256
- package/README.md +1 -1
- 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/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +89 -67
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +3 -3
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +6 -5
- package/dist/discovery/trust-report.md +2 -2
- package/dist/discovery/workspaces.json +5 -4
- package/dist/hooks/dispatch.js +174 -13
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +750 -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/overbuild_lens_contract.ts +196 -0
- package/src/scripts/_lib/release_scope.ts +207 -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_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/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/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 +93 -11
- package/src/scripts/sweep_dead_scan_roots.ts +550 -0
- package/src/scripts/trigger_coverage.ts +11 -37
|
@@ -3,35 +3,51 @@
|
|
|
3
3
|
* Skill-family content-overlap analysis (6.0.0-C Phase 4 Step 8).
|
|
4
4
|
*
|
|
5
5
|
* Ported from the retired Python `src/scripts/audit_skill_overlap.py` (ADR-200 —
|
|
6
|
-
* Python→TS migration, Phase 8 / Wave 8c).
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
6
|
+
* Python→TS migration, Phase 8 / Wave 8c). The numeric contract — keyword
|
|
7
|
+
* vector, cosine, banker's rounding, pair ordering, `json.dumps(..., indent=2)`
|
|
8
|
+
* byte shape, and the Markdown renderer — is preserved so historical reports
|
|
9
|
+
* stay comparable. The CLI grew a `--root` flag; the exit codes are unchanged
|
|
10
|
+
* (0 ok / 3 dead scan scope).
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
* `_skill_roots()`
|
|
14
|
-
*
|
|
15
|
-
*
|
|
12
|
+
* SCAN ROOT (repaired 2026-08-02, road-to-overlap-truth-and-skill-cut Phase 1):
|
|
13
|
+
* the ported `_skill_roots()` resolved the pre-ADR-051 source containers, both
|
|
14
|
+
* of which were deleted when `src/skills` became the source of truth. The tool
|
|
15
|
+
* therefore walked a directory that does not exist and could never report an
|
|
16
|
+
* overlap for the 287-skill corpus. That resolution is GONE, not kept as a
|
|
17
|
+
* fallback branch: the default root is the shared `SRC_SKILLS()` resolver, and
|
|
18
|
+
* an empty scan is a hard failure via {@link assertScanned} rather than an
|
|
19
|
+
* empty result.
|
|
16
20
|
*
|
|
17
21
|
* Walks every `SKILL.md`, builds a keyword vector from the body
|
|
18
22
|
* (frontmatter stripped), and flags pairs whose content cosine-similarity
|
|
19
23
|
* is >= OVERLAP_THRESHOLD. Same-domain (shared `packs:`) >= threshold pairs
|
|
20
24
|
* are the merge candidates. THIS SCRIPT MERGES NOTHING.
|
|
21
25
|
*
|
|
22
|
-
*
|
|
26
|
+
* `--strict` makes a same-pack pair at or above threshold a BUILD FAILURE
|
|
27
|
+
* (exit 1) unless the pair carries a reviewed justification in
|
|
28
|
+
* `audit_skill_overlap_allowlist.json`, capped at 20 entries. The cap is the
|
|
29
|
+
* point: it forces periodic re-litigation instead of an allowlist that grows
|
|
30
|
+
* quietly until the gate means nothing. Cross-pack pairs never block — a
|
|
31
|
+
* cross-pack merge changes install shape and is a different decision.
|
|
23
32
|
*/
|
|
24
33
|
import * as fs from 'node:fs';
|
|
25
34
|
import * as path from 'node:path';
|
|
26
35
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
27
36
|
import { parse as parseYaml } from 'yaml';
|
|
28
37
|
|
|
38
|
+
import { SRC_SKILLS } from './_lib/agent_src.js';
|
|
39
|
+
import { DeadScopeError, assertScanned } from './_lib/scan_scope.js';
|
|
40
|
+
|
|
29
41
|
const _HERE = fileURLToPath(import.meta.url);
|
|
30
42
|
// src/scripts/audit_skill_overlap.py → parent.parent.parent == repo root.
|
|
31
43
|
export const ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
32
44
|
export const REPORT_DIR = path.join(ROOT, 'agents', 'reports');
|
|
33
|
-
export const
|
|
34
|
-
export const
|
|
45
|
+
export const OUT_JSON_NAME = 'skill-overlap.json';
|
|
46
|
+
export const OUT_MD_NAME = 'skill-overlap.md';
|
|
47
|
+
export const OUT_JSON = path.join(REPORT_DIR, OUT_JSON_NAME);
|
|
48
|
+
export const OUT_MD = path.join(REPORT_DIR, OUT_MD_NAME);
|
|
49
|
+
export const ALLOWLIST = path.join(ROOT, 'src', 'scripts', 'audit_skill_overlap_allowlist.json');
|
|
50
|
+
export const ALLOWLIST_CAP = 20;
|
|
35
51
|
// re.compile(r"^---\n(.*?)\n---", re.DOTALL) — used with .search (anywhere).
|
|
36
52
|
const FM_RE = /^---\n([\s\S]*?)\n---/m;
|
|
37
53
|
export const OVERLAP_THRESHOLD = 0.7;
|
|
@@ -70,49 +86,16 @@ function _isFile(p: string): boolean {
|
|
|
70
86
|
return false;
|
|
71
87
|
}
|
|
72
88
|
}
|
|
73
|
-
|
|
74
|
-
/** Sorted immediate child entries (mirrors `sorted(p.iterdir())` / `p.glob`). */
|
|
75
|
-
function _iterdirSorted(p: string): string[] {
|
|
76
|
-
let names: string[];
|
|
77
|
-
try {
|
|
78
|
-
names = fs.readdirSync(p);
|
|
79
|
-
} catch {
|
|
80
|
-
return [];
|
|
81
|
-
}
|
|
82
|
-
names.sort();
|
|
83
|
-
return names.map((n) => path.join(p, n));
|
|
84
|
-
}
|
|
85
|
-
|
|
86
89
|
/**
|
|
87
|
-
*
|
|
88
|
-
* roots = [d for d in (ROOT/"packages").glob("*\/.agent-src.uncondensed/skills")
|
|
89
|
-
* if d.is_dir()] if (ROOT/"packages").is_dir() else []
|
|
90
|
-
* legacy = ROOT/".agent-src.uncondensed"/"skills"
|
|
91
|
-
* if not roots and legacy.is_dir(): roots = [legacy]
|
|
90
|
+
* The one scan root: `src/skills`, via the shared ADR-051 resolver.
|
|
92
91
|
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
* children; the resulting root order only affects first-win dedup, and no
|
|
98
|
-
* skill dir name collides across packages in practice.)
|
|
92
|
+
* There is deliberately no fallback. A second, silently-tried root is how the
|
|
93
|
+
* previous resolution stayed green while reading nothing — the caller passes an
|
|
94
|
+
* explicit `--root` when it wants a different tree (fixtures, tests), and any
|
|
95
|
+
* root that yields zero skills fails loudly in {@link main}.
|
|
99
96
|
*/
|
|
100
|
-
export function
|
|
101
|
-
|
|
102
|
-
let roots: string[] = [];
|
|
103
|
-
if (_isDir(pkgs)) {
|
|
104
|
-
for (const child of _iterdirSorted(pkgs)) {
|
|
105
|
-
const cand = path.join(child, '.agent-src.uncondensed', 'skills');
|
|
106
|
-
if (_isDir(cand)) {
|
|
107
|
-
roots.push(cand);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
const legacy = path.join(ROOT, '.agent-src.uncondensed', 'skills');
|
|
112
|
-
if (roots.length === 0 && _isDir(legacy)) {
|
|
113
|
-
roots = [legacy];
|
|
114
|
-
}
|
|
115
|
-
return roots;
|
|
97
|
+
export function _default_skill_root(): string {
|
|
98
|
+
return SRC_SKILLS();
|
|
116
99
|
}
|
|
117
100
|
|
|
118
101
|
/**
|
|
@@ -162,8 +145,19 @@ export function _cosine(a: Map<string, number>, b: Map<string, number>): number
|
|
|
162
145
|
return da && db ? num / (da * db) : 0.0;
|
|
163
146
|
}
|
|
164
147
|
|
|
148
|
+
/**
|
|
149
|
+
* Which text the similarity vector is built from.
|
|
150
|
+
*
|
|
151
|
+
* `body` is the CANONICAL metric — the one every historical report, the 0.70
|
|
152
|
+
* threshold, and the merge decisions are calibrated against. `description` is
|
|
153
|
+
* an explicitly non-canonical second view: routing happens on descriptions, and
|
|
154
|
+
* body similarity does not measure it, so the two answer different questions
|
|
155
|
+
* and their scores are never mixed or compared against the same threshold.
|
|
156
|
+
*/
|
|
157
|
+
export type Field = 'body' | 'description';
|
|
158
|
+
|
|
165
159
|
/** Mirror `_parse`: yaml.safe_load frontmatter, body after the FM match. */
|
|
166
|
-
export function _parse(md: string): Skill {
|
|
160
|
+
export function _parse(md: string, field: Field = 'body'): Skill {
|
|
167
161
|
const text = fs.readFileSync(md, 'utf-8');
|
|
168
162
|
let fm: Json = {};
|
|
169
163
|
let body = text;
|
|
@@ -179,11 +173,13 @@ export function _parse(md: string): Skill {
|
|
|
179
173
|
const isObj = fm !== null && typeof fm === 'object' && !Array.isArray(fm);
|
|
180
174
|
const name = isObj ? (fm as Record<string, Json>)['name'] : null;
|
|
181
175
|
const packsVal = isObj ? (fm as Record<string, Json>)['packs'] : null;
|
|
176
|
+
const descVal = isObj ? (fm as Record<string, Json>)['description'] : null;
|
|
177
|
+
const source = field === 'description' ? (descVal ? String(descVal) : '') : body;
|
|
182
178
|
return {
|
|
183
179
|
name: name ? String(name) : path.basename(path.dirname(md)),
|
|
184
180
|
relpath: _relPosix(md, ROOT),
|
|
185
181
|
packs: Array.isArray(packsVal) ? new Set(packsVal.map((x) => String(x))) : new Set(),
|
|
186
|
-
vector: _keyword_vector(
|
|
182
|
+
vector: _keyword_vector(source),
|
|
187
183
|
};
|
|
188
184
|
}
|
|
189
185
|
|
|
@@ -247,22 +243,20 @@ function _pathlibCompare(a: string, b: string): number {
|
|
|
247
243
|
return pa.length - pb.length;
|
|
248
244
|
}
|
|
249
245
|
|
|
250
|
-
export function collect(): Skill[] {
|
|
246
|
+
export function collect(root: string = _default_skill_root(), field: Field = 'body'): Skill[] {
|
|
251
247
|
const skills: Skill[] = [];
|
|
252
248
|
const seen = new Set<string>();
|
|
253
|
-
for (const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
continue;
|
|
258
|
-
}
|
|
259
|
-
const key = path.basename(path.dirname(md));
|
|
260
|
-
if (seen.has(key)) {
|
|
261
|
-
continue;
|
|
262
|
-
}
|
|
263
|
-
seen.add(key);
|
|
264
|
-
skills.push(_parse(md));
|
|
249
|
+
for (const md of _rglobSkillMd(root)) {
|
|
250
|
+
// if "_archive" in md.parts: continue
|
|
251
|
+
if (md.split(path.sep).includes('_archive')) {
|
|
252
|
+
continue;
|
|
265
253
|
}
|
|
254
|
+
const key = path.basename(path.dirname(md));
|
|
255
|
+
if (seen.has(key)) {
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
seen.add(key);
|
|
259
|
+
skills.push(_parse(md, field));
|
|
266
260
|
}
|
|
267
261
|
return skills;
|
|
268
262
|
}
|
|
@@ -354,17 +348,30 @@ function _pct(x: number): string {
|
|
|
354
348
|
return `${pct}%`;
|
|
355
349
|
}
|
|
356
350
|
|
|
357
|
-
export function render_md(
|
|
351
|
+
export function render_md(
|
|
352
|
+
skills: Skill[],
|
|
353
|
+
pairs: Pair[],
|
|
354
|
+
threshold: number,
|
|
355
|
+
field: Field = 'body',
|
|
356
|
+
): string {
|
|
358
357
|
const merge = pairs.filter((p) => p.same_domain);
|
|
359
358
|
const cross = pairs.filter((p) => !p.same_domain);
|
|
360
359
|
const L: string[] = [
|
|
361
360
|
'# Skill-family overlap report (6.0.0-C Phase 4 Step 8)\n',
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
361
|
+
field === 'description'
|
|
362
|
+
? `> **NON-CANONICAL measurement.** Cosine over the \`description:\` ` +
|
|
363
|
+
`frontmatter only, across ${skills.length} skills, at ≥ ${_pct(threshold)}. ` +
|
|
364
|
+
`Routing happens on descriptions, so this answers a question the ` +
|
|
365
|
+
`canonical body metric does not — but its scores are NOT comparable ` +
|
|
366
|
+
`to the canonical 0.70 merge threshold and must never be mixed into ` +
|
|
367
|
+
`a merge decision. The canonical run is the one without ` +
|
|
368
|
+
`\`--descriptions\`.\n`
|
|
369
|
+
: `> Content cosine-similarity over ${skills.length} skills; pairs at ` +
|
|
370
|
+
`≥ ${_pct(threshold)}. **Same-domain pairs (shared \`packs:\`) are the ` +
|
|
371
|
+
`merge candidates** consumed by ` +
|
|
372
|
+
`[\`evidence-based-pruning.md\`](../../docs/contracts/evidence-based-pruning.md). ` +
|
|
373
|
+
`This report merges NOTHING — it is input to a future, human-driven ` +
|
|
374
|
+
`consolidation roadmap.\n`,
|
|
368
375
|
`\n- Skills scanned: **${skills.length}**`,
|
|
369
376
|
`\n- Overlap pairs ≥ ${_pct(threshold)}: **${pairs.length}** ` +
|
|
370
377
|
`(${merge.length} same-domain merge candidates, ${cross.length} cross-domain)\n`,
|
|
@@ -476,30 +483,126 @@ class FloatTag {
|
|
|
476
483
|
|
|
477
484
|
export function main(argv: string[] | null = null): number {
|
|
478
485
|
const args = parse_args(argv ?? process.argv.slice(2));
|
|
479
|
-
const
|
|
480
|
-
|
|
481
|
-
|
|
486
|
+
const root = args.root ?? _default_skill_root();
|
|
487
|
+
const skills = collect(root, args.field);
|
|
488
|
+
try {
|
|
489
|
+
assertScanned({
|
|
490
|
+
gate: 'audit_skill_overlap',
|
|
491
|
+
scanned: skills.length,
|
|
492
|
+
units: 'skill(s)',
|
|
493
|
+
roots: [_relPosix(root, ROOT)],
|
|
494
|
+
});
|
|
495
|
+
} catch (exc) {
|
|
496
|
+
if (!(exc instanceof DeadScopeError)) {
|
|
497
|
+
throw exc;
|
|
498
|
+
}
|
|
499
|
+
process.stderr.write(`❌ ${exc.message}\n`);
|
|
482
500
|
return 3;
|
|
483
501
|
}
|
|
484
502
|
const pairs = find_pairs(skills, args.threshold);
|
|
485
|
-
|
|
503
|
+
const outDir = args.outDir ?? REPORT_DIR;
|
|
504
|
+
const outJson = path.join(outDir, OUT_JSON_NAME);
|
|
505
|
+
const outMd = path.join(outDir, OUT_MD_NAME);
|
|
506
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
486
507
|
const jsonObj = {
|
|
487
508
|
threshold: new FloatTag(args.threshold),
|
|
488
509
|
skills_scanned: skills.length,
|
|
489
510
|
pairs: pairs.map(_pairToJson),
|
|
490
511
|
};
|
|
491
|
-
fs.writeFileSync(
|
|
492
|
-
fs.writeFileSync(
|
|
512
|
+
fs.writeFileSync(outJson, _dumpsWithFloats(jsonObj), 'utf-8');
|
|
513
|
+
fs.writeFileSync(outMd, render_md(skills, pairs, args.threshold, args.field), 'utf-8');
|
|
514
|
+
const merge = pairs.filter((p) => p.same_domain);
|
|
493
515
|
if (!args.quiet) {
|
|
494
|
-
const merge = pairs.filter((p) => p.same_domain).length;
|
|
495
516
|
process.stdout.write(
|
|
496
517
|
`✅ Skill overlap: ${skills.length} skills, ${pairs.length} pair(s) ` +
|
|
497
|
-
`≥ ${_pct(args.threshold)} (${merge} same-domain merge candidate(s)).\n`,
|
|
518
|
+
`≥ ${_pct(args.threshold)} (${merge.length} same-domain merge candidate(s)).\n`,
|
|
519
|
+
);
|
|
520
|
+
process.stdout.write(` JSON: ${_relPosix(outJson, ROOT)}\n`);
|
|
521
|
+
process.stdout.write(` MD: ${_relPosix(outMd, ROOT)}\n`);
|
|
522
|
+
}
|
|
523
|
+
// Deliberately NOT gated on --quiet: an advisory CI step whose only output
|
|
524
|
+
// is suppressed by the pipeline's default --quiet is a report nobody reads,
|
|
525
|
+
// which is one step away from the silent-green defect this tool just came
|
|
526
|
+
// back from. The counts line always ships when the table is requested.
|
|
527
|
+
if (args.printTable) {
|
|
528
|
+
process.stdout.write(
|
|
529
|
+
`ℹ️ skill overlap${args.strict ? '' : ' (advisory)'}: ${skills.length} skills scanned, ` +
|
|
530
|
+
`${merge.length} same-domain pair(s) ≥ ${_pct(args.threshold)}\n`,
|
|
531
|
+
);
|
|
532
|
+
for (const p of merge) {
|
|
533
|
+
process.stdout.write(
|
|
534
|
+
` ${p.similarity.toFixed(3)} ${p.a} ↔ ${p.b} [${p.shared_packs.join(', ')}]\n`,
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
if (!args.strict) {
|
|
540
|
+
return 0;
|
|
541
|
+
}
|
|
542
|
+
let allow: Set<string>;
|
|
543
|
+
try {
|
|
544
|
+
allow = _loadAllowlist(args.allowlist ?? ALLOWLIST);
|
|
545
|
+
} catch (exc) {
|
|
546
|
+
process.stderr.write(`❌ ${exc instanceof Error ? exc.message : String(exc)}\n`);
|
|
547
|
+
return 2;
|
|
548
|
+
}
|
|
549
|
+
const blocking = merge.filter((p) => !allow.has(_pairKey(p.a, p.b)));
|
|
550
|
+
if (blocking.length === 0) {
|
|
551
|
+
return 0;
|
|
552
|
+
}
|
|
553
|
+
process.stderr.write(
|
|
554
|
+
`❌ audit_skill_overlap: ${blocking.length} same-pack pair(s) at or above ` +
|
|
555
|
+
`${_pct(args.threshold)} with no reviewed justification:\n`,
|
|
556
|
+
);
|
|
557
|
+
for (const p of blocking) {
|
|
558
|
+
process.stderr.write(
|
|
559
|
+
` ${p.similarity.toFixed(3)} ${p.a} ↔ ${p.b} [${p.shared_packs.join(', ')}]\n`,
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
process.stderr.write(
|
|
563
|
+
` Merge the pair, or add {"pair": "${_pairKey(blocking[0]!.a, blocking[0]!.b)}", "reason": "…"} ` +
|
|
564
|
+
`to ${_relPosix(args.allowlist ?? ALLOWLIST, ROOT)} (cap ${ALLOWLIST_CAP}).\n`,
|
|
565
|
+
);
|
|
566
|
+
return 1;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/** Order-independent key for an unordered pair. */
|
|
570
|
+
export function _pairKey(a: string, b: string): string {
|
|
571
|
+
return a < b ? `${a}::${b}` : `${b}::${a}`;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Reviewed same-pack overlaps, capped. Over-cap throws rather than warning:
|
|
576
|
+
* per the autonomous-execution allowlist-growth antipattern, an allowlist that
|
|
577
|
+
* outgrows its cap means the threshold is wrong, not that the corpus needs
|
|
578
|
+
* twenty-one exceptions.
|
|
579
|
+
*/
|
|
580
|
+
export function _loadAllowlist(file: string): Set<string> {
|
|
581
|
+
if (!_isFile(file)) {
|
|
582
|
+
return new Set();
|
|
583
|
+
}
|
|
584
|
+
const data = JSON.parse(fs.readFileSync(file, 'utf-8')) as {
|
|
585
|
+
entries?: Array<{ pair: string; reason: string }>;
|
|
586
|
+
};
|
|
587
|
+
const entries = Array.isArray(data.entries) ? data.entries : [];
|
|
588
|
+
if (entries.length > ALLOWLIST_CAP) {
|
|
589
|
+
throw new Error(
|
|
590
|
+
`audit_skill_overlap: allowlist has ${entries.length} entries (> ${ALLOWLIST_CAP}). ` +
|
|
591
|
+
'Per the autonomous-execution allowlist-growth antipattern, the threshold is wrong, ' +
|
|
592
|
+
'not the corpus — re-litigate the bar, do not grow the list.',
|
|
498
593
|
);
|
|
499
|
-
process.stdout.write(` JSON: ${_relPosix(OUT_JSON, ROOT)}\n`);
|
|
500
|
-
process.stdout.write(` MD: ${_relPosix(OUT_MD, ROOT)}\n`);
|
|
501
594
|
}
|
|
502
|
-
|
|
595
|
+
const out = new Set<string>();
|
|
596
|
+
for (const e of entries) {
|
|
597
|
+
const [a, b] = String(e.pair).split('::');
|
|
598
|
+
if (a === undefined || b === undefined || (e.reason ?? '').trim() === '') {
|
|
599
|
+
throw new Error(
|
|
600
|
+
`audit_skill_overlap: allowlist entry ${JSON.stringify(e)} needs "pair": "a::b" and a non-empty "reason".`,
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
out.add(_pairKey(a, b));
|
|
604
|
+
}
|
|
605
|
+
return out;
|
|
503
606
|
}
|
|
504
607
|
|
|
505
608
|
/** json.dumps(indent=2) with FloatTag → Python-float repr. */
|
|
@@ -545,23 +648,58 @@ function _dumpsWithFloats(obj: Json, level = 0): string {
|
|
|
545
648
|
interface Args {
|
|
546
649
|
threshold: number;
|
|
547
650
|
quiet: boolean;
|
|
651
|
+
/** Absolute scan root; `undefined` = the `src/skills` default. */
|
|
652
|
+
root?: string;
|
|
653
|
+
/** Absolute report directory; `undefined` = `agents/reports`. */
|
|
654
|
+
outDir?: string;
|
|
655
|
+
/** Print the same-domain pair table to stdout even under `--quiet`. */
|
|
656
|
+
printTable: boolean;
|
|
657
|
+
/** Fail the build on an unjustified same-pack pair at or above threshold. */
|
|
658
|
+
strict: boolean;
|
|
659
|
+
/** Allowlist path override (tests); `undefined` = the shipped file. */
|
|
660
|
+
allowlist?: string;
|
|
661
|
+
/** Which text the vector is built from. `description` is NON-canonical. */
|
|
662
|
+
field: Field;
|
|
548
663
|
}
|
|
549
664
|
|
|
550
665
|
export function parse_args(argv: string[]): Args {
|
|
551
|
-
const args: Args = {
|
|
666
|
+
const args: Args = {
|
|
667
|
+
threshold: OVERLAP_THRESHOLD,
|
|
668
|
+
quiet: false,
|
|
669
|
+
printTable: false,
|
|
670
|
+
strict: false,
|
|
671
|
+
field: 'body',
|
|
672
|
+
};
|
|
673
|
+
const takeValue = (flag: string, inline: string | undefined, next: () => string | undefined): string => {
|
|
674
|
+
const v = inline ?? next();
|
|
675
|
+
if (v === undefined) {
|
|
676
|
+
process.stderr.write(`argument ${flag}: expected one argument\n`);
|
|
677
|
+
process.exit(2);
|
|
678
|
+
}
|
|
679
|
+
return v;
|
|
680
|
+
};
|
|
552
681
|
for (let i = 0; i < argv.length; i++) {
|
|
553
682
|
const a = argv[i] as string;
|
|
554
683
|
if (a === '--quiet') {
|
|
555
684
|
args.quiet = true;
|
|
556
|
-
} else if (a === '--
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
args.
|
|
563
|
-
} else if (a
|
|
564
|
-
args.
|
|
685
|
+
} else if (a === '--strict') {
|
|
686
|
+
args.strict = true;
|
|
687
|
+
} else if (a === '--allowlist' || a.startsWith('--allowlist=')) {
|
|
688
|
+
const inline = a.startsWith('--allowlist=') ? a.slice('--allowlist='.length) : undefined;
|
|
689
|
+
args.allowlist = path.resolve(ROOT, takeValue('--allowlist', inline, () => argv[++i]));
|
|
690
|
+
} else if (a === '--descriptions') {
|
|
691
|
+
args.field = 'description';
|
|
692
|
+
} else if (a === '--print-table') {
|
|
693
|
+
args.printTable = true;
|
|
694
|
+
} else if (a === '--threshold' || a.startsWith('--threshold=')) {
|
|
695
|
+
const inline = a.startsWith('--threshold=') ? a.slice('--threshold='.length) : undefined;
|
|
696
|
+
args.threshold = Number(takeValue('--threshold', inline, () => argv[++i]));
|
|
697
|
+
} else if (a === '--root' || a.startsWith('--root=')) {
|
|
698
|
+
const inline = a.startsWith('--root=') ? a.slice('--root='.length) : undefined;
|
|
699
|
+
args.root = path.resolve(ROOT, takeValue('--root', inline, () => argv[++i]));
|
|
700
|
+
} else if (a === '--out-dir' || a.startsWith('--out-dir=')) {
|
|
701
|
+
const inline = a.startsWith('--out-dir=') ? a.slice('--out-dir='.length) : undefined;
|
|
702
|
+
args.outDir = path.resolve(ROOT, takeValue('--out-dir', inline, () => argv[++i]));
|
|
565
703
|
} else {
|
|
566
704
|
process.stderr.write(`unrecognized arguments: ${a}\n`);
|
|
567
705
|
process.exit(2);
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "Reviewed same-pack overlaps at or above the canonical 0.70 threshold. Each entry needs a real reason — a pair is here because a human read both skills and decided the overlap is structural, not because the gate was noisy. Capped at 20 by audit_skill_overlap.ts: crossing the cap means the threshold is wrong, not that the corpus needs a twenty-first exception. Empty is the healthy state.",
|
|
3
|
+
"entries": []
|
|
4
|
+
}
|
|
@@ -146,6 +146,11 @@ function render(): string {
|
|
|
146
146
|
const entries = [...ledger.values()].sort((a, b) => a.id.localeCompare(b.id));
|
|
147
147
|
const backed = entries.filter((e) => e.status === 'backed');
|
|
148
148
|
const unbacked = entries.filter((e) => e.status === 'unbacked');
|
|
149
|
+
// `resolved-null` is neither backed nor debt: the question was asked, the
|
|
150
|
+
// answer came back null, and the entry is closed. Counting it as debt
|
|
151
|
+
// would inflate the inventory with items that can never shrink; counting
|
|
152
|
+
// it as backed would claim a win that was never measured.
|
|
153
|
+
const resolvedNull = entries.filter((e) => e.status === 'resolved-null');
|
|
149
154
|
|
|
150
155
|
const L: string[] = [];
|
|
151
156
|
L.push('<!-- GENERATED by src/scripts/build_proof.ts — do NOT hand-edit.');
|
|
@@ -232,6 +237,13 @@ function render(): string {
|
|
|
232
237
|
L.push('allowed to carry a marker in public prose. Hiding them would be the');
|
|
233
238
|
L.push('opposite of the point.');
|
|
234
239
|
}
|
|
240
|
+
if (resolvedNull.length > 0) {
|
|
241
|
+
L.push('');
|
|
242
|
+
L.push(`And our **nulls**: ${resolvedNull.length} claim(s) are \`resolved-null\` —`);
|
|
243
|
+
L.push('measured, the threshold was missed, and the entry is closed rather than');
|
|
244
|
+
L.push('left open forever. A null that stays filed as pending debt is a claim');
|
|
245
|
+
L.push('quietly waiting to be re-argued.');
|
|
246
|
+
}
|
|
235
247
|
L.push('');
|
|
236
248
|
L.push('## 2. We publish honest nulls');
|
|
237
249
|
L.push('');
|
|
@@ -419,9 +431,11 @@ function render(): string {
|
|
|
419
431
|
{
|
|
420
432
|
const ledger = [...load_ledger().values()].sort((a, b) => a.id.localeCompare(b.id));
|
|
421
433
|
const backed = ledger.filter((e) => e.status === 'backed').length;
|
|
434
|
+
const nulls = ledger.filter((e) => e.status === 'resolved-null').length;
|
|
422
435
|
L.push(
|
|
423
436
|
`**Axis 2 — evidence form per public claim.** ${ledger.length} ledger entries · ` +
|
|
424
|
-
`${backed} backed · ${ledger.length - backed} unbacked inventory
|
|
437
|
+
`${backed} backed · ${ledger.length - backed - nulls} unbacked inventory · ` +
|
|
438
|
+
`${nulls} resolved-null.`,
|
|
425
439
|
);
|
|
426
440
|
L.push('');
|
|
427
441
|
L.push('| Claim id | Kind | Status | Evidence pointer |');
|
|
@@ -44,11 +44,14 @@ const REPO_ROOT = path.resolve(fileURLToPath(import.meta.url), '..', '..', '..')
|
|
|
44
44
|
* exact pin cannot suffer the freshest-patch failure (the version is either
|
|
45
45
|
* cached or it is not), but it also forfeits patch fixes — so each one is
|
|
46
46
|
* named here rather than silently tolerated.
|
|
47
|
+
*
|
|
48
|
+
* Empty since the code-graph parser pair (`web-tree-sitter` /
|
|
49
|
+
* `tree-sitter-wasms`) left `dependencies` — it was the only entry. The pair's
|
|
50
|
+
* ABI pin now lives in `code_graph/loader.ts`'s install hint, because that is
|
|
51
|
+
* the only place a re-enabler reads it; this gate scans `dependencies` and so
|
|
52
|
+
* no longer sees the pair at all.
|
|
47
53
|
*/
|
|
48
|
-
export const EXACT_PIN_EXCEPTIONS: Readonly<Record<string, string>> = {
|
|
49
|
-
'web-tree-sitter': 'ABI-locked against tree-sitter-wasms; see code-graph engine pin',
|
|
50
|
-
'tree-sitter-wasms': 'ABI-locked against web-tree-sitter; see code-graph engine pin',
|
|
51
|
-
};
|
|
54
|
+
export const EXACT_PIN_EXCEPTIONS: Readonly<Record<string, string>> = {};
|
|
52
55
|
|
|
53
56
|
/**
|
|
54
57
|
* Floors that are non-settled ON PURPOSE, because every version below them is
|
|
@@ -74,8 +77,15 @@ const EXACT_VERSION = /^\d+\.\d+\.\d+$/;
|
|
|
74
77
|
/**
|
|
75
78
|
* Every violation in `dependencies`, as human-readable lines. Empty array
|
|
76
79
|
* means the manifest cannot produce a freshest-patch `ETARGET` for a consumer.
|
|
80
|
+
*
|
|
81
|
+
* `exactPinExceptions` defaults to the module's list and is injectable so the
|
|
82
|
+
* allow-an-exact-pin branch stays covered now that the real list is empty —
|
|
83
|
+
* otherwise the only way to test it would be to add a fake production entry.
|
|
77
84
|
*/
|
|
78
|
-
export function evaluate(
|
|
85
|
+
export function evaluate(
|
|
86
|
+
dependencies: Readonly<Record<string, string>>,
|
|
87
|
+
exactPinExceptions: Readonly<Record<string, string>> = EXACT_PIN_EXCEPTIONS,
|
|
88
|
+
): string[] {
|
|
79
89
|
const errors: string[] = [];
|
|
80
90
|
for (const [name, range] of Object.entries(dependencies)) {
|
|
81
91
|
const security = SECURITY_FLOOR_EXCEPTIONS[name];
|
|
@@ -89,7 +99,7 @@ export function evaluate(dependencies: Readonly<Record<string, string>>): string
|
|
|
89
99
|
continue;
|
|
90
100
|
}
|
|
91
101
|
if (EXACT_VERSION.test(range)) {
|
|
92
|
-
if (!(name in
|
|
102
|
+
if (!(name in exactPinExceptions)) {
|
|
93
103
|
errors.push(
|
|
94
104
|
`${name}@${range}: exact pin without an entry in EXACT_PIN_EXCEPTIONS — ` +
|
|
95
105
|
'add the reason there, or use a settled caret floor (^X.Y.0)',
|
|
@@ -31,13 +31,13 @@
|
|
|
31
31
|
* Exit codes: 0 all enforced gates cleared their floor · 1 a gate is blind,
|
|
32
32
|
* collapsed, or silent · 2 the manifest itself is missing/empty/malformed.
|
|
33
33
|
*/
|
|
34
|
-
import { spawnSync } from 'node:child_process';
|
|
35
34
|
import * as fs from 'node:fs';
|
|
36
35
|
import * as path from 'node:path';
|
|
37
36
|
import process from 'node:process';
|
|
38
37
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
39
38
|
|
|
40
39
|
import yaml from 'js-yaml';
|
|
40
|
+
import { runCountedProbe } from './_lib/counted_probe.js';
|
|
41
41
|
|
|
42
42
|
const _HERE = fileURLToPath(import.meta.url);
|
|
43
43
|
const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
@@ -184,11 +184,11 @@ export function classify(
|
|
|
184
184
|
|
|
185
185
|
function run_gate(spec: GateSpec): { scanned: number | null; crashed: boolean; exit_code: number | null } {
|
|
186
186
|
const runner = path.join(REPO_ROOT, 'scripts-run');
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
});
|
|
191
|
-
if (r.
|
|
187
|
+
// Bounded read: a gate whose output overflowed would lose its `scanned:`
|
|
188
|
+
// line and be classified `silent` — the scan-scope guard reporting a dead
|
|
189
|
+
// scope that is merely truncated. `runCountedProbe` throws instead.
|
|
190
|
+
const r = runCountedProbe(runner, [`src/scripts/${spec.id}`, ...spec.argv], { cwd: REPO_ROOT });
|
|
191
|
+
if (r.failure !== null && r.status === null) return { scanned: null, crashed: true, exit_code: null };
|
|
192
192
|
// A gate may legitimately exit non-zero (it found real violations) and still
|
|
193
193
|
// have scanned plenty — coverage and verdict are different questions.
|
|
194
194
|
return { scanned: parse_scanned(`${r.stdout}\n${r.stderr}`), crashed: false, exit_code: r.status };
|
|
@@ -36,6 +36,7 @@ import * as fs from 'node:fs';
|
|
|
36
36
|
import * as path from 'node:path';
|
|
37
37
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
38
38
|
import { KERNEL_RULE_FILENAMES } from './_lib/kernel_rules.js';
|
|
39
|
+
import { runCountedProbe } from './_lib/counted_probe.js';
|
|
39
40
|
|
|
40
41
|
const KERNEL_RULES: ReadonlySet<string> = KERNEL_RULE_FILENAMES;
|
|
41
42
|
|
|
@@ -56,13 +57,13 @@ const KERNEL_DIR = 'src/rules';
|
|
|
56
57
|
const DEFAULT_LABEL = 'bundled-always-rules-acknowledged';
|
|
57
58
|
|
|
58
59
|
function _git_changed_files(base_ref: string): string[] {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
});
|
|
60
|
+
// Bounded read: a truncated diff would shrink the change set and let the
|
|
61
|
+
// bundle guard pass on files it never saw.
|
|
62
|
+
const res = runCountedProbe('git', ['diff', '--name-only', `${base_ref}...HEAD`]);
|
|
62
63
|
// Python uses check_output(stderr=STDOUT, text=True): on non-zero it raises
|
|
63
64
|
// CalledProcessError and the handler prints the combined output. spawnSync
|
|
64
65
|
// does not raise; mirror by checking status and combining stdout+stderr.
|
|
65
|
-
if (res.
|
|
66
|
+
if (!res.ok) {
|
|
66
67
|
const combined = `${res.stdout ?? ''}${res.stderr ?? ''}`;
|
|
67
68
|
process.stderr.write(`❌ git diff failed: ${combined.trim()}\n`);
|
|
68
69
|
return [];
|
|
@@ -113,20 +113,6 @@ export function router_triggers(router: Json): Map<string, Array<Record<string,
|
|
|
113
113
|
return map;
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
// trigger_coverage.ts word semantics: lowercase alpha-led tokens, len > 2.
|
|
117
|
-
const _WORD = /[a-z][a-z0-9_]+/g;
|
|
118
|
-
|
|
119
|
-
function _tokens(text: string): Set<string> {
|
|
120
|
-
const out = new Set<string>();
|
|
121
|
-
const low = text.toLowerCase();
|
|
122
|
-
_WORD.lastIndex = 0;
|
|
123
|
-
let m: RegExpExecArray | null;
|
|
124
|
-
while ((m = _WORD.exec(low)) !== null) {
|
|
125
|
-
if (m[0].length > 2) out.add(m[0]);
|
|
126
|
-
}
|
|
127
|
-
return out;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
116
|
/**
|
|
131
117
|
* Whether a task exercises ≥1 trigger of the rule (prompt↔trigger
|
|
132
118
|
* falsifiability, Phase 3). Kernel rules always fire.
|
|
@@ -140,16 +126,12 @@ export function task_fires_rule(
|
|
|
140
126
|
if (kernel.has(ruleId)) return true;
|
|
141
127
|
const triggers = router_triggers(router).get(ruleId) ?? [];
|
|
142
128
|
const low = task.prompt.toLowerCase();
|
|
143
|
-
const toks = _tokens(task.prompt);
|
|
144
129
|
const files = Array.isArray(task.context_files) ? task.context_files : [];
|
|
145
130
|
const cmd = typeof task.command === 'string' ? task.command : '';
|
|
146
131
|
for (const trig of triggers) {
|
|
147
132
|
if ('keyword' in trig || 'phrase' in trig) {
|
|
148
133
|
const needle = String(trig.keyword ?? trig.phrase).toLowerCase();
|
|
149
134
|
if (needle && low.includes(needle)) return true;
|
|
150
|
-
} else if ('intent' in trig) {
|
|
151
|
-
const words = _tokens(String(trig.intent));
|
|
152
|
-
if (words.size > 0 && [...words].every((w) => toks.has(w))) return true;
|
|
153
135
|
} else if ('path_prefix' in trig) {
|
|
154
136
|
const prefix = String(trig.path_prefix);
|
|
155
137
|
if (files.some((f) => f.startsWith(prefix))) return true;
|