@event4u/agent-config 8.8.0 → 8.10.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 +139 -263
- package/README.md +5 -5
- package/dist/agent-src/commands/agent-handoff.md +39 -2
- package/dist/agent-src/commands/cost/report.md +13 -3
- package/dist/agent-src/commands/research/deep.md +8 -0
- package/dist/agent-src/commands/research/report.md +7 -0
- package/dist/agent-src/commands/roadmap/create.md +12 -0
- package/dist/agent-src/commands/roadmap/process-full.md +31 -5
- package/dist/agent-src/commands/tests/create.md +28 -6
- package/dist/agent-src/commands/work.md +33 -0
- package/dist/agent-src/commands/worktree/cleanup.md +78 -0
- package/dist/agent-src/commands/worktree/create.md +62 -0
- package/dist/agent-src/commands/worktree/status.md +63 -0
- package/dist/agent-src/commands/worktree/verify.md +70 -0
- package/dist/agent-src/commands/worktree.md +85 -0
- package/dist/agent-src/contexts/communication/rules-auto/think-before-action-mechanics.md +30 -1
- package/dist/agent-src/contexts/execution/orchestration-telemetry.md +60 -31
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +37 -0
- package/dist/agent-src/contexts/execution/subagent-topologies.md +34 -0
- package/dist/agent-src/contexts/execution/verification-mechanics.md +9 -9
- package/dist/agent-src/rules/agent-authority.md +1 -1
- package/dist/agent-src/rules/communication-through-line.md +1 -1
- package/dist/agent-src/rules/delegation-policy.md +5 -0
- package/dist/agent-src/rules/design-fidelity.md +40 -0
- package/dist/agent-src/rules/direct-answers.md +1 -1
- package/dist/agent-src/rules/downstream-changes.md +1 -0
- package/dist/agent-src/rules/external-code-graph-interop.md +65 -0
- package/dist/agent-src/rules/git-history-discipline.md +18 -0
- package/dist/agent-src/rules/lethal-trifecta-guard.md +6 -1
- package/dist/agent-src/rules/minimal-safe-diff.md +58 -10
- package/dist/agent-src/rules/non-destructive-by-default.md +3 -0
- package/dist/agent-src/rules/question-not-instruction.md +76 -0
- package/dist/agent-src/rules/senior-engineering-discipline.md +1 -1
- package/dist/agent-src/rules/spreadsheet-source-quality.md +61 -0
- package/dist/agent-src/rules/think-before-action.md +1 -0
- package/dist/agent-src/rules/tool-safety.md +8 -0
- package/dist/agent-src/rules/untrusted-input-defense.md +87 -22
- package/dist/agent-src/scripts/update_roadmap_progress.ts +2 -1
- package/dist/agent-src/skills/accessibility-auditor/SKILL.md +19 -0
- package/dist/agent-src/skills/accessibility-auditor/evals/evals.json +15 -0
- package/dist/agent-src/skills/ai-code-blindspots/SKILL.md +10 -3
- package/dist/agent-src/skills/analysis-skill-router/evals/evals.json +13 -0
- package/dist/agent-src/skills/api-testing/SKILL.md +15 -1
- package/dist/agent-src/skills/character-consistency/evals/evals.json +16 -0
- package/dist/agent-src/skills/code-review/SKILL.md +2 -0
- package/dist/agent-src/skills/command-routing/evals/evals.json +14 -0
- package/dist/agent-src/skills/command-writing/SKILL.md +53 -40
- package/dist/agent-src/skills/dashboard-design/evals/evals.json +16 -0
- package/dist/agent-src/skills/dcf-modeling/evals/domain-truth.json +29 -0
- package/dist/agent-src/skills/dcf-modeling/evals/evals.json +20 -0
- package/dist/agent-src/skills/decision-record/evals/evals.json +20 -0
- package/dist/agent-src/skills/deep-reading-analyst/SKILL.md +1 -0
- package/dist/agent-src/skills/design-intelligence/SKILL.md +49 -12
- package/dist/agent-src/skills/design-intelligence/evals/evals.json +14 -0
- package/dist/agent-src/skills/design-review/SKILL.md +35 -15
- package/dist/agent-src/skills/design-system-capture/SKILL.md +84 -24
- package/dist/agent-src/skills/design-system-capture/evals/evals.json +15 -0
- package/dist/agent-src/skills/developer-like-execution/evals/evals.json +16 -0
- package/dist/agent-src/skills/doc-coauthoring/SKILL.md +27 -0
- package/dist/agent-src/skills/doc-coauthoring/evals/evals.json +16 -0
- package/dist/agent-src/skills/editorial-calendar/evals/domain-truth.json +16 -0
- package/dist/agent-src/skills/editorial-calendar/evals/evals.json +16 -0
- package/dist/agent-src/skills/estimate-ticket/evals/evals.json +16 -0
- package/dist/agent-src/skills/existing-ui-audit/SKILL.md +30 -0
- package/dist/agent-src/skills/existing-ui-audit/evals/triggers.json +47 -0
- package/dist/agent-src/skills/fe-design/SKILL.md +36 -5
- package/dist/agent-src/skills/fe-design/evals/triggers.json +47 -0
- package/dist/agent-src/skills/forecasting/evals/domain-truth.json +29 -0
- package/dist/agent-src/skills/forecasting/evals/evals.json +20 -0
- package/dist/agent-src/skills/frontend-render-security/SKILL.md +2 -2
- package/dist/agent-src/skills/fundraising-narrative/evals/domain-truth.json +16 -0
- package/dist/agent-src/skills/fundraising-narrative/evals/evals.json +16 -0
- package/dist/agent-src/skills/html-deck/SKILL.md +27 -0
- package/dist/agent-src/skills/iconography/SKILL.md +24 -0
- package/dist/agent-src/skills/incident-commander/evals/domain-truth.json +16 -0
- package/dist/agent-src/skills/incident-commander/evals/evals.json +20 -0
- package/dist/agent-src/skills/launch-readiness/evals/evals.json +21 -0
- package/dist/agent-src/skills/logging-monitoring/evals/evals.json +16 -0
- package/dist/agent-src/skills/markitdown/SKILL.md +1 -0
- package/dist/agent-src/skills/mcp/SKILL.md +24 -0
- package/dist/agent-src/skills/mcp-builder/SKILL.md +9 -1
- package/dist/agent-src/skills/memory-consolidation/SKILL.md +55 -4
- package/dist/agent-src/skills/messaging-architecture/evals/domain-truth.json +16 -0
- package/dist/agent-src/skills/messaging-architecture/evals/evals.json +16 -0
- package/dist/agent-src/skills/perf-feedback-craft/evals/evals.json +16 -0
- package/dist/agent-src/skills/pest-testing/SKILL.md +1 -1
- package/dist/agent-src/skills/playwright-testing/SKILL.md +16 -0
- package/dist/agent-src/skills/prompt-engineering-patterns/evals/evals.json +16 -0
- package/dist/agent-src/skills/refine-prompt/evals/evals.json +16 -0
- package/dist/agent-src/skills/refine-ticket/evals/evals.json +16 -0
- package/dist/agent-src/skills/release-comms/evals/evals.json +16 -0
- package/dist/agent-src/skills/rice-prioritization/evals/evals.json +20 -0
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +39 -5
- package/dist/agent-src/skills/runway-cognition/evals/domain-truth.json +29 -0
- package/dist/agent-src/skills/runway-cognition/evals/evals.json +20 -0
- package/dist/agent-src/skills/scenario-modeling/evals/domain-truth.json +29 -0
- package/dist/agent-src/skills/scenario-modeling/evals/evals.json +20 -0
- package/dist/agent-src/skills/secrets-management/SKILL.md +7 -0
- package/dist/agent-src/skills/skill-writing/SKILL.md +12 -3
- package/dist/agent-src/skills/spreadsheet-authoring/SKILL.md +98 -0
- package/dist/agent-src/skills/spreadsheet-authoring/evals/triggers.json +47 -0
- package/dist/agent-src/skills/subagent-orchestration/SKILL.md +27 -33
- package/dist/agent-src/skills/supply-chain-intake/SKILL.md +12 -0
- package/dist/agent-src/skills/systematic-debugging/evals/evals.json +20 -0
- package/dist/agent-src/skills/test-case-discovery/SKILL.md +118 -0
- package/dist/agent-src/skills/test-case-discovery/evals/triggers.json +52 -0
- package/dist/agent-src/skills/test-driven-development/SKILL.md +32 -6
- package/dist/agent-src/skills/test-driven-development/evals/evals.json +16 -0
- package/dist/agent-src/skills/testing-anti-patterns/SKILL.md +34 -4
- package/dist/agent-src/skills/threat-modeling/evals/evals.json +21 -0
- package/dist/agent-src/skills/typography-system/evals/evals.json +14 -0
- package/dist/agent-src/skills/ui-component-architect/SKILL.md +9 -0
- package/dist/agent-src/skills/unit-economics-modeling/evals/domain-truth.json +40 -0
- package/dist/agent-src/skills/unit-economics-modeling/evals/evals.json +24 -0
- package/dist/agent-src/skills/verify-completion-evidence/evals/evals.json +16 -0
- package/dist/agent-src/skills/vision-articulation/evals/evals.json +16 -0
- package/dist/agent-src/skills/voice-and-tone-design/evals/evals.json +16 -0
- package/dist/agent-src/skills/worktree-lifecycle/SKILL.md +180 -0
- package/dist/agent-src/skills/worktree-lifecycle/evals/triggers.json +52 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +36 -0
- package/dist/cli/registry.js +3 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +326 -67
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +6 -6
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +25 -14
- package/dist/discovery/trust-report.md +4 -4
- package/dist/discovery/workspaces.json +25 -12
- package/dist/install/install.mjs +136 -1
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/claude_builtin_names.js +145 -0
- package/dist/scripts/_lib/claude_builtin_names.js.map +1 -0
- package/dist/scripts/condense.js +14 -0
- package/dist/scripts/condense.js.map +1 -1
- package/docs/CLAIMS.md +36 -8
- package/docs/architecture.md +4 -4
- package/docs/archive/CHANGELOG-pre-8.9.0.md +306 -0
- package/docs/benchmark.md +113 -0
- package/docs/catalog.md +32 -8
- package/docs/command-flows.md +6 -1
- package/docs/contracts/CHANGELOG-conventions.md +6 -0
- package/docs/contracts/command-clusters.md +18 -0
- package/docs/contracts/design-artifact-lifecycle.md +93 -0
- package/docs/contracts/design-artifact-verification.md +153 -0
- package/docs/contracts/injected-block-authenticity-model.md +66 -0
- package/docs/contracts/mcp-tool-inventory.md +28 -28
- package/docs/contracts/release-pr-gating.md +26 -0
- package/docs/contracts/release-sizing.md +85 -0
- package/docs/contracts/surface-agent-contracts.md +153 -0
- package/docs/decisions/ADR-061-corpus-grounding-layer.md +13 -0
- package/docs/decisions/ADR-115-command-cluster-consolidation-phase-4.md +1 -1
- package/docs/decisions/ADR-116-memory-tripwire-activation-path.md +9 -0
- package/docs/distribution/consumer-matrix.md +96 -0
- package/docs/featured-skills.md +2 -2
- package/docs/getting-started-by-role.md +1 -1
- package/docs/getting-started.md +2 -2
- package/docs/governance-advantage.md +1 -1
- package/docs/guidelines/agent-infra/asking-and-brevity-examples.md +27 -0
- package/docs/guidelines/agent-infra/emphasis-budget.md +45 -0
- package/docs/guidelines/agent-infra/simplicity-and-goal-demos.md +151 -0
- package/docs/guidelines/agent-infra/tool-description-as-policy.md +35 -0
- package/docs/guidelines/design-canon.md +1 -0
- package/docs/proof.md +38 -23
- package/docs/release-runbook.md +3 -0
- package/docs/second-brain-scope.md +69 -26
- package/docs/self-review-gate.md +51 -0
- package/package.json +1 -1
- package/src/agent-src/scripts/update_roadmap_progress.ts +2 -1
- package/src/scripts/_dispatch.bash +27 -0
- package/src/scripts/_lib/claude_builtin_names.ts +148 -0
- package/src/scripts/_lib/file_slicer.ts +117 -0
- package/src/scripts/_lib/lexical_index.ts +155 -0
- package/src/scripts/_lib/orchestration_record.ts +183 -0
- package/src/scripts/_lib/orchestration_savings.ts +114 -9
- package/src/scripts/_lib/retrieval_sanitize.ts +71 -0
- package/src/scripts/_lib/security_lint.ts +1 -0
- package/src/scripts/_lib/stat_index.ts +98 -0
- package/src/scripts/benchmark.ts +122 -0
- package/src/scripts/build_proof.ts +30 -17
- package/src/scripts/check_memory.ts +2 -0
- package/src/scripts/check_quality_regression.ts +51 -0
- package/src/scripts/check_release_adjacent_health.ts +121 -0
- package/src/scripts/check_roadmap_trackable.ts +3 -2
- package/src/scripts/check_rule_invariants.ts +275 -0
- package/src/scripts/condense.ts +14 -0
- package/src/scripts/consumer_matrix.ts +353 -0
- package/src/scripts/discovery_graph.ts +0 -0
- package/src/scripts/external_sources_denylist.json +4 -1
- package/src/scripts/generate_index.ts +6 -2
- package/src/scripts/install-hooks.sh +25 -0
- package/src/scripts/install.ts +35 -2
- package/src/scripts/learning_sidecar.ts +0 -0
- package/src/scripts/lint_agent_skill_names.ts +29 -0
- package/src/scripts/lint_changelog_rollback.ts +293 -0
- package/src/scripts/lint_knowledge_scale.ts +4 -3
- package/src/scripts/lint_roadmap_complexity.ts +124 -0
- package/src/scripts/lint_versioned_cache.ts +195 -0
- package/src/scripts/mcp_server/consumer_tool_catalog.json +2 -1
- package/src/scripts/mcp_server/tools.ts +24 -1
- package/src/scripts/measure_lexical_ranking.ts +173 -0
- package/src/scripts/memory_lookup.ts +0 -0
- package/src/scripts/orchestration_record.ts +156 -0
- package/src/scripts/orchestration_savings_report.ts +65 -8
- package/src/scripts/schemas/skill.schema.json +4 -0
- package/src/scripts/score_domain_truth.ts +192 -0
- package/src/scripts/second_brain_retrieval.ts +320 -0
- package/src/scripts/second_brain_run.ts +341 -0
- package/src/scripts/self_review_gate.test.ts +65 -0
- package/src/scripts/self_review_gate.ts +272 -0
- package/src/scripts/trigger_eval_grandfather.json +223 -224
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Deterministic domain-truth scorer (road-to-domain-soundness Phase 3).
|
|
4
|
+
*
|
|
5
|
+
* Pure, LLM-free scoring for the `deterministic` half of the domain-truth
|
|
6
|
+
* fixtures (`skills/<skill>/evals/domain-truth.json`). Given a skill's run
|
|
7
|
+
* output for a case and the case's `expected` + `tolerance`, it extracts the
|
|
8
|
+
* numeric answer and returns pass/fail. It does NOT invoke any model — the
|
|
9
|
+
* expensive part (running the skill against the scenario on a fixed host) is
|
|
10
|
+
* the maintainer's Phase-3 gate; this only scores a captured output.
|
|
11
|
+
*
|
|
12
|
+
* Scoring contract (kept simple + gameable-input-resistant):
|
|
13
|
+
* - Prefer an explicit `ANSWER: <n>` line (case-insensitive) — the run
|
|
14
|
+
* prompt instructs the skill to end with one, so the echoed scenario
|
|
15
|
+
* numbers cannot be mistaken for the answer.
|
|
16
|
+
* - Fallback: the last numeric token in the output.
|
|
17
|
+
* - Normalization: strip `$`, thousands separators, surrounding whitespace,
|
|
18
|
+
* and a trailing unit token (`%`, `months`, `mo`, `x`, or stray letters).
|
|
19
|
+
* Scenarios fix the unit (e.g. "state in $M"), so no magnitude conversion
|
|
20
|
+
* is applied — a unit-instruction miss is a real fail, not a scorer bug.
|
|
21
|
+
* - Pass iff |extracted - expected| <= tolerance.
|
|
22
|
+
*
|
|
23
|
+
* Rubric cases are NOT scored here (they need a pinned judge, recorded as a
|
|
24
|
+
* known-limit) — this scorer skips any case whose `check.kind !== "deterministic"`.
|
|
25
|
+
*/
|
|
26
|
+
import * as fs from 'node:fs';
|
|
27
|
+
import * as path from 'node:path';
|
|
28
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
29
|
+
|
|
30
|
+
export interface DeterministicCheck {
|
|
31
|
+
kind: 'deterministic';
|
|
32
|
+
expected: number | string;
|
|
33
|
+
tolerance?: number;
|
|
34
|
+
rationale: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface CaseResult {
|
|
38
|
+
id: string;
|
|
39
|
+
pass: boolean;
|
|
40
|
+
expected: number;
|
|
41
|
+
tolerance: number;
|
|
42
|
+
extracted: number | null;
|
|
43
|
+
reason: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Pull a single numeric answer out of a skill's run output. */
|
|
47
|
+
export function extractAnswer(output: string): number | null {
|
|
48
|
+
if (typeof output !== 'string' || output.trim() === '') return null;
|
|
49
|
+
// Prefer an explicit ANSWER: line (last one wins if repeated).
|
|
50
|
+
const answerLines = output.match(/ANSWER:\s*([^\n\r]+)/gi);
|
|
51
|
+
if (answerLines && answerLines.length > 0) {
|
|
52
|
+
const last = answerLines[answerLines.length - 1] as string;
|
|
53
|
+
const n = _firstNumber(last.replace(/ANSWER:\s*/i, ''));
|
|
54
|
+
if (n !== null) return n;
|
|
55
|
+
}
|
|
56
|
+
// Fallback: the last numeric token anywhere in the output.
|
|
57
|
+
return _lastNumber(output);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Normalize a fragment ("$1,350M", "7.5 months", "32%") to a bare number. */
|
|
61
|
+
function _firstNumber(fragment: string): number | null {
|
|
62
|
+
const cleaned = fragment.replace(/[$,]/g, '');
|
|
63
|
+
const m = cleaned.match(/-?\d+(?:\.\d+)?/);
|
|
64
|
+
if (!m) return null;
|
|
65
|
+
const v = Number(m[0]);
|
|
66
|
+
return Number.isFinite(v) ? v : null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function _lastNumber(text: string): number | null {
|
|
70
|
+
const cleaned = text.replace(/[$,]/g, '');
|
|
71
|
+
const all = cleaned.match(/-?\d+(?:\.\d+)?/g);
|
|
72
|
+
if (!all || all.length === 0) return null;
|
|
73
|
+
const v = Number(all[all.length - 1]);
|
|
74
|
+
return Number.isFinite(v) ? v : null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Score one deterministic case against a captured output string. */
|
|
78
|
+
export function scoreDeterministic(output: string, check: DeterministicCheck): {
|
|
79
|
+
pass: boolean;
|
|
80
|
+
extracted: number | null;
|
|
81
|
+
expected: number;
|
|
82
|
+
tolerance: number;
|
|
83
|
+
reason: string;
|
|
84
|
+
} {
|
|
85
|
+
const expected = typeof check.expected === 'number' ? check.expected : Number(check.expected);
|
|
86
|
+
const tolerance = check.tolerance ?? 0;
|
|
87
|
+
const extracted = extractAnswer(output);
|
|
88
|
+
if (extracted === null || !Number.isFinite(expected)) {
|
|
89
|
+
return {
|
|
90
|
+
pass: false,
|
|
91
|
+
extracted,
|
|
92
|
+
expected,
|
|
93
|
+
tolerance,
|
|
94
|
+
reason: extracted === null ? 'no numeric answer found in output' : 'expected is not numeric',
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
const delta = Math.abs(extracted - expected);
|
|
98
|
+
const pass = delta <= tolerance;
|
|
99
|
+
return {
|
|
100
|
+
pass,
|
|
101
|
+
extracted,
|
|
102
|
+
expected,
|
|
103
|
+
tolerance,
|
|
104
|
+
reason: pass
|
|
105
|
+
? `|${extracted} - ${expected}| = ${_round(delta)} <= ${tolerance}`
|
|
106
|
+
: `|${extracted} - ${expected}| = ${_round(delta)} > ${tolerance}`,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function _round(n: number): number {
|
|
111
|
+
return Math.round(n * 1e6) / 1e6;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Score a whole fixture against a map of {caseId -> captured output}. Rubric
|
|
116
|
+
* cases are reported as `skipped` (kind !== deterministic), never silently
|
|
117
|
+
* passed.
|
|
118
|
+
*/
|
|
119
|
+
export function scoreFixture(
|
|
120
|
+
fixture: { skill: string; cases: Array<{ id: string; check: { kind: string; [k: string]: unknown } }> },
|
|
121
|
+
outputs: Record<string, string>,
|
|
122
|
+
): { skill: string; results: CaseResult[]; skipped: string[] } {
|
|
123
|
+
const results: CaseResult[] = [];
|
|
124
|
+
const skipped: string[] = [];
|
|
125
|
+
for (const c of fixture.cases) {
|
|
126
|
+
if (c.check.kind !== 'deterministic') {
|
|
127
|
+
skipped.push(c.id);
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
const output = outputs[c.id] ?? '';
|
|
131
|
+
const r = scoreDeterministic(output, c.check as unknown as DeterministicCheck);
|
|
132
|
+
results.push({
|
|
133
|
+
id: c.id,
|
|
134
|
+
pass: r.pass,
|
|
135
|
+
expected: r.expected,
|
|
136
|
+
tolerance: r.tolerance,
|
|
137
|
+
extracted: r.extracted,
|
|
138
|
+
reason: output === '' ? 'no captured output for this case' : r.reason,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
return { skill: fixture.skill, results, skipped };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
145
|
+
export const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* CLI: `score_domain_truth <skill> <outputs.json>` where outputs.json is a
|
|
149
|
+
* `{ "<caseId>": "<captured skill output>" }` map. Prints a per-case table and
|
|
150
|
+
* exits 0 iff every deterministic case passes (1 otherwise).
|
|
151
|
+
*/
|
|
152
|
+
function _runCli(argv: string[]): number {
|
|
153
|
+
const [skill, outputsPath] = argv;
|
|
154
|
+
if (!skill || !outputsPath) {
|
|
155
|
+
process.stderr.write('usage: score_domain_truth <skill> <outputs.json>\n');
|
|
156
|
+
return 2;
|
|
157
|
+
}
|
|
158
|
+
const fixturePath = path.join(REPO_ROOT, 'src', 'skills', skill, 'evals', 'domain-truth.json');
|
|
159
|
+
if (!fs.existsSync(fixturePath)) {
|
|
160
|
+
process.stderr.write(`error: no domain-truth.json for skill '${skill}' at ${fixturePath}\n`);
|
|
161
|
+
return 1;
|
|
162
|
+
}
|
|
163
|
+
const fixture = JSON.parse(fs.readFileSync(fixturePath, 'utf-8'));
|
|
164
|
+
const outputs = JSON.parse(fs.readFileSync(path.resolve(outputsPath), 'utf-8')) as Record<string, string>;
|
|
165
|
+
const { results, skipped } = scoreFixture(fixture, outputs);
|
|
166
|
+
process.stdout.write(`domain-truth run — ${skill}\n`);
|
|
167
|
+
for (const r of results) {
|
|
168
|
+
const mark = r.pass ? '✅' : '❌';
|
|
169
|
+
process.stdout.write(` ${mark} ${r.id}: extracted=${r.extracted} expected=${r.expected} (±${r.tolerance}) — ${r.reason}\n`);
|
|
170
|
+
}
|
|
171
|
+
if (skipped.length > 0) {
|
|
172
|
+
process.stdout.write(` · skipped (rubric, needs pinned judge): ${skipped.join(', ')}\n`);
|
|
173
|
+
}
|
|
174
|
+
const passed = results.filter((r) => r.pass).length;
|
|
175
|
+
process.stdout.write(` ${passed}/${results.length} deterministic cases pass\n`);
|
|
176
|
+
return passed === results.length && results.length > 0 ? 0 : 1;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function _isCliEntry(): boolean {
|
|
180
|
+
if (process.argv[1] === undefined) return false;
|
|
181
|
+
const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
182
|
+
if (import.meta.url === argvUrl) return true;
|
|
183
|
+
try {
|
|
184
|
+
return fs.realpathSync(fileURLToPath(import.meta.url)) === fs.realpathSync(path.resolve(process.argv[1]));
|
|
185
|
+
} catch {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (_isCliEntry()) {
|
|
191
|
+
process.exit(_runCli(process.argv.slice(2)));
|
|
192
|
+
}
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Second-brain RETRIEVAL-PRECISION follow-up
|
|
4
|
+
* (road-to-second-brain-retrieval-precision).
|
|
5
|
+
*
|
|
6
|
+
* The Phase-2 delta run (road-to-second-brain-delta-proof) injected the exact
|
|
7
|
+
* prior fact — a context-value UPPER BOUND assuming perfect retrieval. This
|
|
8
|
+
* follow-up removes that assumption: it runs the REAL `memory_lookup` retrieval
|
|
9
|
+
* against a populated decision store whose distractors deliberately share query
|
|
10
|
+
* keywords with the needed entry, and measures:
|
|
11
|
+
*
|
|
12
|
+
* - precision@k — did retrieval surface the NEEDED decision in the top-k?
|
|
13
|
+
* DETERMINISTIC + free (no model), so reported even in
|
|
14
|
+
* --dry-run. This is the substrate's real retrieval quality.
|
|
15
|
+
* - end-to-end — three arms, scored like Phase 2:
|
|
16
|
+
* retrieval-on : inject the top-k RETRIEVED entries (confusers included)
|
|
17
|
+
* retrieval-off : no memory
|
|
18
|
+
* placebo : inject k FIXED unrelated entries (equal count, no query)
|
|
19
|
+
* retrieval-on vs off/placebo tests whether keyword-retrieved context — with
|
|
20
|
+
* whatever confuser noise it carries — actually helps the model answer.
|
|
21
|
+
*
|
|
22
|
+
* A low precision@k is a legitimate, honest finding: it is evidence for the
|
|
23
|
+
* lint_knowledge_scale / SQLite-FTS5 activation path (ADR-116), not a failure.
|
|
24
|
+
*
|
|
25
|
+
* Modes: --dry-run (real precision@k + stubbed arms, no spend) · --run (live
|
|
26
|
+
* arms, SPEND) · --host <id> · --seeds <n>. Exit 0 ok / 1 error / 2 usage.
|
|
27
|
+
*/
|
|
28
|
+
import * as fs from 'node:fs';
|
|
29
|
+
import * as path from 'node:path';
|
|
30
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
31
|
+
|
|
32
|
+
import * as YAML from 'yaml';
|
|
33
|
+
|
|
34
|
+
import { load_anthropic_key } from './ai_council/clients.js';
|
|
35
|
+
import {
|
|
36
|
+
_setIntakeRoot,
|
|
37
|
+
_setKnowledgeRoot,
|
|
38
|
+
_setMemoryRoot,
|
|
39
|
+
retrieve,
|
|
40
|
+
} from './memory_lookup.js';
|
|
41
|
+
import { sanitize_text } from './_lib/retrieval_sanitize.js';
|
|
42
|
+
import { scoreTask } from './second_brain_score.js';
|
|
43
|
+
|
|
44
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
45
|
+
export const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
46
|
+
export const STORE_DIR = path.join(REPO_ROOT, 'internal', 'bench', 'second-brain', 'retrieval-store');
|
|
47
|
+
export const REPORT_DIR = path.join(REPO_ROOT, 'internal', 'bench', 'reports');
|
|
48
|
+
|
|
49
|
+
const DEFAULT_HOST = 'claude-haiku-4-5-20251001';
|
|
50
|
+
const DEFAULT_SEEDS = 3;
|
|
51
|
+
/** Fixed unrelated entries for the placebo arm (equal-count inert context). */
|
|
52
|
+
const PLACEBO_IDS = ['dist-ci-runner', 'dist-lint-strict', 'dist-docs-site', 'dist-monorepo'];
|
|
53
|
+
|
|
54
|
+
export type Arm = 'retrieval-off' | 'retrieval-on' | 'placebo';
|
|
55
|
+
export const ARMS: readonly Arm[] = ['retrieval-off', 'retrieval-on', 'placebo'];
|
|
56
|
+
|
|
57
|
+
export interface RetrievalTask {
|
|
58
|
+
id: string;
|
|
59
|
+
query: string[];
|
|
60
|
+
needed: string;
|
|
61
|
+
prompt: string;
|
|
62
|
+
answer_key: { must_contain: string[]; must_not_contain: string[] };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function bindStore(): void {
|
|
66
|
+
_setMemoryRoot(STORE_DIR);
|
|
67
|
+
_setKnowledgeRoot(path.join(STORE_DIR, 'knowledge-none'));
|
|
68
|
+
_setIntakeRoot(path.join(STORE_DIR, 'intake-none'));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function loadCorpus(): {
|
|
72
|
+
type: string;
|
|
73
|
+
k: number;
|
|
74
|
+
tasks: RetrievalTask[];
|
|
75
|
+
stale_ids?: string[];
|
|
76
|
+
poisoned_ids?: string[];
|
|
77
|
+
} {
|
|
78
|
+
const doc = YAML.parse(fs.readFileSync(path.join(STORE_DIR, 'retrieval-corpus.yml'), 'utf-8')) as {
|
|
79
|
+
type: string;
|
|
80
|
+
k: number;
|
|
81
|
+
tasks: RetrievalTask[];
|
|
82
|
+
stale_ids?: string[];
|
|
83
|
+
poisoned_ids?: string[];
|
|
84
|
+
};
|
|
85
|
+
return doc;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Retrieve for a task's query. Returns the top-k bodies to inject PLUS the
|
|
90
|
+
* discrimination diagnostics the keyword scorer's tie behaviour demands:
|
|
91
|
+
* - rank of the needed entry in the full sorted list (-1 if absent)
|
|
92
|
+
* - tie_set_size: how many entries share the TOP score. The scorer gives 0.8
|
|
93
|
+
* to any keyword match and breaks ties by store order (not relevance), so a
|
|
94
|
+
* tie_set_size > 1 means retrieval did NOT rank the needed entry by
|
|
95
|
+
* relevance — it recalled it into a bucket. This is the honest
|
|
96
|
+
* discrimination signal (ADR-116 evidence), separate from precision@k.
|
|
97
|
+
*/
|
|
98
|
+
export function retrieveFor(
|
|
99
|
+
task: RetrievalTask,
|
|
100
|
+
type: string,
|
|
101
|
+
k: number,
|
|
102
|
+
): { topk: Array<{ id: string; body: string }>; rank: number; tieSetSize: number; topScore: number } {
|
|
103
|
+
const all = retrieve([type], task.query, 100);
|
|
104
|
+
const topScore = all.length ? all[0]!.score : 0;
|
|
105
|
+
const tieSetSize = all.filter((h) => h.score === topScore).length;
|
|
106
|
+
const rank = all.findIndex((h) => h.id === task.needed);
|
|
107
|
+
const topk = all.slice(0, k).map((h) => ({ id: h.id, body: String((h.entry as { body?: unknown }).body ?? '') }));
|
|
108
|
+
return { topk, rank, tieSetSize, topScore };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Read the fixed placebo entries' bodies straight from the store file. */
|
|
112
|
+
function _placeboBodies(type: string): string[] {
|
|
113
|
+
const doc = YAML.parse(fs.readFileSync(path.join(STORE_DIR, `${type}.yml`), 'utf-8')) as {
|
|
114
|
+
entries: Array<{ id: string; body?: string }>;
|
|
115
|
+
};
|
|
116
|
+
const byId = new Map(doc.entries.map((e) => [e.id, e.body ?? '']));
|
|
117
|
+
return PLACEBO_IDS.map((id) => byId.get(id) ?? '').filter(Boolean);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function _prompt(task: RetrievalTask, arm: Arm, retrieved: string[], placebo: string[]): string {
|
|
121
|
+
const preamble =
|
|
122
|
+
'You are continuing a multi-session engineering project. Answer using the ' +
|
|
123
|
+
'project decisions on record. Be concise and concrete.';
|
|
124
|
+
if (arm === 'retrieval-off') return `${preamble}\n\n${task.prompt}`;
|
|
125
|
+
const bodies = arm === 'retrieval-on' ? retrieved : placebo;
|
|
126
|
+
const block =
|
|
127
|
+
'Retrieved project memory:\n' + bodies.map((b) => `- ${b.trim()}`).join('\n');
|
|
128
|
+
return `${preamble}\n\n${block}\n\n${task.prompt}`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
interface CallResult { text: string; tokens_in: number; tokens_out: number }
|
|
132
|
+
async function callAnthropic(model: string, prompt: string, apiKey: string): Promise<CallResult> {
|
|
133
|
+
const res = await fetch('https://api.anthropic.com/v1/messages', {
|
|
134
|
+
method: 'POST',
|
|
135
|
+
headers: { 'content-type': 'application/json', 'x-api-key': apiKey, 'anthropic-version': '2023-06-01' },
|
|
136
|
+
body: JSON.stringify({ model, max_tokens: 256, temperature: 0.7, messages: [{ role: 'user', content: prompt }] }),
|
|
137
|
+
});
|
|
138
|
+
if (!res.ok) throw new Error(`anthropic ${res.status}: ${(await res.text()).slice(0, 300)}`);
|
|
139
|
+
const data = (await res.json()) as {
|
|
140
|
+
content?: Array<{ type: string; text?: string }>;
|
|
141
|
+
usage?: { input_tokens?: number; output_tokens?: number };
|
|
142
|
+
};
|
|
143
|
+
const text = (data.content ?? []).filter((b) => b.type === 'text').map((b) => b.text ?? '').join('');
|
|
144
|
+
return { text, tokens_in: data.usage?.input_tokens ?? 0, tokens_out: data.usage?.output_tokens ?? 0 };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function _scoreObj(task: RetrievalTask) {
|
|
148
|
+
return { id: task.id, metric: 'retrieval-accuracy' as const, session_k: '', session_k1_prompt: task.prompt, answer_key: task.answer_key };
|
|
149
|
+
}
|
|
150
|
+
function stub(task: RetrievalTask, arm: Arm, neededRetrieved: boolean): string {
|
|
151
|
+
// retrieval-on passes ONLY if the needed entry was actually retrieved.
|
|
152
|
+
if (arm === 'retrieval-on' && neededRetrieved) {
|
|
153
|
+
return `${task.answer_key.must_contain.join(' ')} — per the retrieved decision.`;
|
|
154
|
+
}
|
|
155
|
+
return "I don't have that decision on record here.";
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Two-sided sign-test p-value via exact binomial (ties excluded). */
|
|
159
|
+
export function signTestP(wins: number, losses: number): number {
|
|
160
|
+
const n = wins + losses;
|
|
161
|
+
if (n === 0) return 1;
|
|
162
|
+
const k = Math.min(wins, losses);
|
|
163
|
+
let cum = 0;
|
|
164
|
+
for (let i = 0; i <= k; i++) {
|
|
165
|
+
let c = 1;
|
|
166
|
+
for (let j = 0; j < i; j++) c = (c * (n - j)) / (j + 1);
|
|
167
|
+
cum += c;
|
|
168
|
+
}
|
|
169
|
+
return Math.min(1, (cum / 2 ** n) * 2);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export async function run(opts: { mode: 'dry-run' | 'live'; host: string; seeds: number }): Promise<Record<string, unknown>> {
|
|
173
|
+
bindStore();
|
|
174
|
+
const { type, k, tasks, stale_ids = [], poisoned_ids = [] } = loadCorpus();
|
|
175
|
+
const staleSet = new Set(stale_ids);
|
|
176
|
+
const poisonedSet = new Set(poisoned_ids);
|
|
177
|
+
const apiKey = opts.mode === 'live' ? load_anthropic_key() : '';
|
|
178
|
+
const placebo = _placeboBodies(type);
|
|
179
|
+
|
|
180
|
+
let precisionHits = 0;
|
|
181
|
+
let topkSlots = 0;
|
|
182
|
+
let staleHits = 0;
|
|
183
|
+
let poisonedRetrieved = 0;
|
|
184
|
+
let poisonedNeutralized = 0;
|
|
185
|
+
let tieSetSum = 0;
|
|
186
|
+
const perTask: Array<Record<string, unknown>> = [];
|
|
187
|
+
const armPass: Record<Arm, number> = { 'retrieval-off': 0, 'retrieval-on': 0, placebo: 0 };
|
|
188
|
+
const armTotal: Record<Arm, number> = { 'retrieval-off': 0, 'retrieval-on': 0, placebo: 0 };
|
|
189
|
+
const taskArmPass: Record<string, Record<Arm, number>> = {};
|
|
190
|
+
let calls = 0, tIn = 0, tOut = 0;
|
|
191
|
+
|
|
192
|
+
for (const task of tasks) {
|
|
193
|
+
const { topk, rank, tieSetSize } = retrieveFor(task, type, k);
|
|
194
|
+
const ids = topk.map((r) => r.id);
|
|
195
|
+
topkSlots += topk.length;
|
|
196
|
+
for (const hit of topk) {
|
|
197
|
+
if (staleSet.has(hit.id)) staleHits += 1;
|
|
198
|
+
if (poisonedSet.has(hit.id)) {
|
|
199
|
+
poisonedRetrieved += 1;
|
|
200
|
+
// Rejection = the sanitize floor strips every hidden-instruction
|
|
201
|
+
// vector before the body could reach a prompt.
|
|
202
|
+
const clean = sanitize_text(hit.body);
|
|
203
|
+
if (!/[---]/u.test(clean)) poisonedNeutralized += 1;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const neededInTopK = rank >= 0 && rank < k;
|
|
208
|
+
if (neededInTopK) precisionHits += 1;
|
|
209
|
+
tieSetSum += tieSetSize;
|
|
210
|
+
const tap: Record<Arm, number> = { 'retrieval-off': 0, 'retrieval-on': 0, placebo: 0 };
|
|
211
|
+
|
|
212
|
+
for (const arm of ARMS) {
|
|
213
|
+
for (let s = 0; s < opts.seeds; s++) {
|
|
214
|
+
let transcript: string;
|
|
215
|
+
if (opts.mode === 'dry-run') {
|
|
216
|
+
transcript = stub(task, arm, neededInTopK);
|
|
217
|
+
} else {
|
|
218
|
+
const r = await callAnthropic(opts.host, _prompt(task, arm, topk.map((x) => x.body), placebo), apiKey);
|
|
219
|
+
transcript = r.text; calls += 1; tIn += r.tokens_in; tOut += r.tokens_out;
|
|
220
|
+
}
|
|
221
|
+
armTotal[arm] += 1;
|
|
222
|
+
if (scoreTask(transcript, _scoreObj(task)).pass) { armPass[arm] += 1; tap[arm] += 1; }
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
taskArmPass[task.id] = tap;
|
|
226
|
+
perTask.push({ id: task.id, needed: task.needed, needed_rank: rank, needed_in_top_k: neededInTopK, tie_set_size: tieSetSize, top_k_ids: ids });
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const precisionAtK = tasks.length ? precisionHits / tasks.length : 0;
|
|
230
|
+
const meanTieSet = tasks.length ? tieSetSum / tasks.length : 0;
|
|
231
|
+
const pair = (a: Arm, b: Arm) => {
|
|
232
|
+
let aw = 0, bw = 0, ties = 0;
|
|
233
|
+
for (const t of tasks) {
|
|
234
|
+
const tap = taskArmPass[t.id]!;
|
|
235
|
+
const pa = tap[a], pb = tap[b];
|
|
236
|
+
if (pa > pb) aw += 1; else if (pb > pa) bw += 1; else ties += 1;
|
|
237
|
+
}
|
|
238
|
+
return { a_wins: aw, b_wins: bw, ties, sign_p: signTestP(aw, bw) };
|
|
239
|
+
};
|
|
240
|
+
const onOff = pair('retrieval-on', 'retrieval-off');
|
|
241
|
+
const onPlacebo = pair('retrieval-on', 'placebo');
|
|
242
|
+
const verdict =
|
|
243
|
+
onOff.a_wins > onOff.b_wins && onOff.sign_p < 0.05 &&
|
|
244
|
+
onPlacebo.a_wins > onPlacebo.b_wins && onPlacebo.sign_p < 0.05 ? 'PASS' : 'NULL';
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
schema_version: 1,
|
|
248
|
+
kind: `second-brain-retrieval-${opts.mode}`,
|
|
249
|
+
roadmap: 'road-to-second-brain-retrieval-precision',
|
|
250
|
+
host: opts.host,
|
|
251
|
+
seeds: opts.seeds,
|
|
252
|
+
mode: opts.mode,
|
|
253
|
+
k,
|
|
254
|
+
store: 'internal/bench/second-brain/retrieval-store/ (5 needed + 19 distractors, keyword-overlapping confusers)',
|
|
255
|
+
precision_at_k: precisionAtK,
|
|
256
|
+
// road-to-proof-under-real-conditions Phase 5 — retrieval quality pair:
|
|
257
|
+
// stale_hit_rate: fraction of retrieved top-k slots occupied by
|
|
258
|
+
// SUPERSEDED store entries (lower is better; the substrate has no
|
|
259
|
+
// recency/supersede weighting yet, so a non-zero rate is the honest
|
|
260
|
+
// baseline the FTS5/recency activation path improves on).
|
|
261
|
+
stale_hit_rate: topkSlots ? staleHits / topkSlots : 0,
|
|
262
|
+
stale_hits: staleHits,
|
|
263
|
+
topk_slots: topkSlots,
|
|
264
|
+
// poisoned_rejection_rate: of poisoned entries that WERE retrieved,
|
|
265
|
+
// the fraction whose hidden-instruction vectors the sanitize floor
|
|
266
|
+
// strips (target 1.0; null when none was retrieved this run).
|
|
267
|
+
poisoned_rejection_rate: poisonedRetrieved ? poisonedNeutralized / poisonedRetrieved : null,
|
|
268
|
+
poisoned_retrieved: poisonedRetrieved,
|
|
269
|
+
poisoned_neutralized: poisonedNeutralized,
|
|
270
|
+
|
|
271
|
+
precision_hits: precisionHits,
|
|
272
|
+
mean_tie_set_size: meanTieSet,
|
|
273
|
+
discrimination_note:
|
|
274
|
+
'mean_tie_set_size > 1 means the keyword scorer gave the same top score to multiple ' +
|
|
275
|
+
'entries and broke the tie by STORE ORDER, not relevance — precision@k reflects recall ' +
|
|
276
|
+
'into the top-k, not relevance ranking. This is the ADR-116 (FTS5) signal.',
|
|
277
|
+
tasks: tasks.length,
|
|
278
|
+
per_task: perTask,
|
|
279
|
+
aggregate: Object.fromEntries(ARMS.map((a) => [a, { pass: armPass[a], total: armTotal[a] }])),
|
|
280
|
+
paired: { 'on-vs-off': onOff, 'on-vs-placebo': onPlacebo },
|
|
281
|
+
verdict,
|
|
282
|
+
note:
|
|
283
|
+
'precision@k is deterministic (retrieval only, no spend); the arms are model-scored. ' +
|
|
284
|
+
'A retrieval-on PASS means keyword retrieval both FOUND the right decision under confuser ' +
|
|
285
|
+
'load AND the model used it; a NULL with high precision isolates a model-use gap, a NULL ' +
|
|
286
|
+
'with low precision is evidence for the FTS5 activation path (ADR-116).',
|
|
287
|
+
cost: { calls, tokens_in: tIn, tokens_out: tOut },
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export async function main(argv: readonly string[] = process.argv.slice(2)): Promise<number> {
|
|
292
|
+
const mode = argv.includes('--run') ? 'live' : argv.includes('--dry-run') ? 'dry-run' : null;
|
|
293
|
+
if (mode === null) { process.stderr.write('usage: second_brain_retrieval --dry-run | --run [--host <id>] [--seeds <n>]\n'); return 2; }
|
|
294
|
+
const hi = argv.indexOf('--host'); const host = hi >= 0 ? String(argv[hi + 1]) : DEFAULT_HOST;
|
|
295
|
+
const si = argv.indexOf('--seeds'); const seeds = si >= 0 ? Number(argv[si + 1]) : DEFAULT_SEEDS;
|
|
296
|
+
let report: Record<string, unknown>;
|
|
297
|
+
try { report = await run({ mode, host, seeds }); } catch (e) { process.stderr.write(`second_brain_retrieval: ${String(e)}\n`); return 1; }
|
|
298
|
+
if (mode === 'live') {
|
|
299
|
+
fs.mkdirSync(REPORT_DIR, { recursive: true });
|
|
300
|
+
const out = path.join(REPORT_DIR, 'second-brain-retrieval.json');
|
|
301
|
+
fs.writeFileSync(out, JSON.stringify(report, null, 2) + '\n');
|
|
302
|
+
process.stdout.write(`wrote ${path.relative(REPO_ROOT, out)}\n`);
|
|
303
|
+
}
|
|
304
|
+
const a = report.aggregate as Record<Arm, { pass: number; total: number }>;
|
|
305
|
+
process.stdout.write(
|
|
306
|
+
`${mode}: precision@${report.k}=${((report.precision_at_k as number) * 100).toFixed(0)}% (${report.precision_hits}/${report.tasks}) · ` +
|
|
307
|
+
`mean-tie-set=${(report.mean_tie_set_size as number).toFixed(1)} · ` +
|
|
308
|
+
`on=${a['retrieval-on'].pass}/${a['retrieval-on'].total} off=${a['retrieval-off'].pass}/${a['retrieval-off'].total} placebo=${a.placebo.pass}/${a.placebo.total} · ` +
|
|
309
|
+
`${report.verdict} · calls=${(report.cost as { calls: number }).calls}\n`,
|
|
310
|
+
);
|
|
311
|
+
return 0;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function _isCliEntry(): boolean {
|
|
315
|
+
if (process.argv[1] === undefined) return false;
|
|
316
|
+
const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
317
|
+
if (import.meta.url === argvUrl) return true;
|
|
318
|
+
try { return fs.realpathSync(fileURLToPath(import.meta.url)) === fs.realpathSync(path.resolve(process.argv[1])); } catch { return false; }
|
|
319
|
+
}
|
|
320
|
+
if (_isCliEntry()) { main().then((code) => process.exit(code)); }
|