@event4u/agent-config 8.7.0 → 8.9.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 +109 -222
- package/CONTRIBUTING.md +25 -0
- package/README.md +7 -4
- package/dist/agent-src/commands/agent-handoff.md +39 -2
- 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/contexts/communication/rules-auto/think-before-action-mechanics.md +30 -1
- package/dist/agent-src/contexts/execution/auto-orchestration-activation.md +1 -1
- package/dist/agent-src/contexts/execution/orchestration-benchmark-gate.md +28 -16
- package/dist/agent-src/contexts/execution/orchestration-telemetry.md +36 -5
- 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/active-remediation.md +92 -0
- package/dist/agent-src/rules/agent-authority.md +1 -1
- package/dist/agent-src/rules/broken-access-control.md +106 -0
- package/dist/agent-src/rules/communication-through-line.md +42 -0
- package/dist/agent-src/rules/decision-revisit-gate.md +8 -0
- package/dist/agent-src/rules/delegation-policy.md +5 -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 +66 -1
- package/dist/agent-src/rules/non-destructive-by-default.md +3 -0
- package/dist/agent-src/rules/prefer-enums-over-literals.md +65 -0
- package/dist/agent-src/rules/question-not-instruction.md +76 -0
- package/dist/agent-src/rules/senior-engineering-discipline.md +82 -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/ai-code-blindspots/SKILL.md +112 -0
- package/dist/agent-src/skills/ai-code-blindspots/evals/triggers.json +52 -0
- package/dist/agent-src/skills/authz-review/SKILL.md +21 -4
- package/dist/agent-src/skills/code-review/SKILL.md +2 -0
- package/dist/agent-src/skills/command-writing/SKILL.md +53 -40
- package/dist/agent-src/skills/dcf-modeling/evals/domain-truth.json +29 -0
- package/dist/agent-src/skills/deep-reading-analyst/SKILL.md +1 -0
- package/dist/agent-src/skills/dependency-upgrade/SKILL.md +20 -0
- package/dist/agent-src/skills/design-intelligence/SKILL.md +49 -12
- package/dist/agent-src/skills/design-review/SKILL.md +29 -15
- package/dist/agent-src/skills/design-system-capture/SKILL.md +63 -24
- package/dist/agent-src/skills/forecasting/evals/domain-truth.json +29 -0
- package/dist/agent-src/skills/frontend-render-security/SKILL.md +104 -0
- package/dist/agent-src/skills/frontend-render-security/evals/triggers.json +52 -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/roadmap-writing/SKILL.md +46 -5
- package/dist/agent-src/skills/runway-cognition/evals/domain-truth.json +29 -0
- package/dist/agent-src/skills/scenario-modeling/evals/domain-truth.json +29 -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/subagent-orchestration/SKILL.md +27 -33
- package/dist/agent-src/skills/supply-chain-intake/SKILL.md +112 -0
- package/dist/agent-src/skills/supply-chain-intake/evals/triggers.json +52 -0
- package/dist/agent-src/skills/testing-anti-patterns/SKILL.md +72 -2
- 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/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +60 -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 +268 -46
- 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 +18 -8
- package/dist/discovery/trust-report.md +11 -11
- package/dist/discovery/workspaces.json +16 -6
- package/dist/install/install.mjs +141 -3
- package/dist/mcp/awesome-mcp-servers.row.md +1 -1
- package/dist/mcp/mcp-cloudflare-catalogue.json +1 -1
- package/dist/mcp/registry-manifest.json +4 -4
- 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/dist/server/schemas/settings.js +2 -1
- package/dist/server/schemas/settings.js.map +1 -1
- package/docs/CLAIMS.md +58 -2
- package/docs/architecture.md +3 -3
- package/docs/archive/CHANGELOG-pre-8.9.0.md +306 -0
- package/docs/benchmark.md +113 -0
- package/docs/catalog.md +32 -5
- package/docs/contracts/command-clusters.md +18 -0
- package/docs/contracts/design-artifact-verification.md +124 -0
- package/docs/contracts/injected-block-authenticity-model.md +66 -0
- package/docs/contracts/mcp-tool-inventory.md +31 -30
- package/docs/decisions/ADR-061-corpus-grounding-layer.md +13 -0
- package/docs/decisions/ADR-105-automatic-subagent-orchestration.md +6 -0
- package/docs/decisions/ADR-116-memory-tripwire-activation-path.md +11 -2
- package/docs/decisions/ADR-117-subagents-auto-default-on.md +88 -0
- package/docs/decisions/INDEX.md +1 -0
- package/docs/experiences/content_creator.md +2 -0
- package/docs/experiences/finance.md +2 -0
- package/docs/experiences/founder.md +2 -0
- package/docs/experiences/ops.md +2 -0
- package/docs/featured-skills.md +2 -2
- package/docs/getting-started-by-role.md +1 -1
- package/docs/getting-started.md +1 -1
- 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/memory-access.md +12 -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 +60 -3
- package/docs/release-runbook.md +138 -0
- package/docs/second-brain-scope.md +116 -0
- package/docs/succession.md +80 -0
- package/package.json +2 -2
- package/src/agent-src/scripts/update_roadmap_progress.ts +2 -1
- package/src/config/agent-settings.template.yml +25 -6
- 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_gate.ts +12 -8
- package/src/scripts/_lib/orchestration_record.ts +170 -0
- package/src/scripts/_lib/orchestration_savings.ts +180 -0
- 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 +51 -0
- package/src/scripts/chat_history.ts +79 -7
- package/src/scripts/check_memory.ts +2 -0
- package/src/scripts/check_quality_regression.ts +51 -0
- package/src/scripts/check_roadmap_trackable.ts +3 -2
- package/src/scripts/condense.ts +14 -0
- package/src/scripts/discovery_graph.ts +0 -0
- package/src/scripts/domain_soundness_status.ts +236 -0
- package/src/scripts/external_sources_denylist.json +4 -1
- package/src/scripts/generate_index.ts +17 -0
- package/src/scripts/hot_context_hook.ts +47 -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_behavioural_eval_freshness.ts +140 -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 +23 -3
- package/src/scripts/mcp_server/tools.ts +182 -1
- package/src/scripts/measure_lexical_ranking.ts +173 -0
- package/src/scripts/memory_lookup.ts +0 -0
- package/src/scripts/memory_replay.ts +285 -0
- package/src/scripts/orchestration_record.ts +140 -0
- package/src/scripts/orchestration_savings_report.ts +124 -0
- package/src/scripts/schemas/domain-truth.schema.json +94 -0
- package/src/scripts/schemas/evals.schema.json +113 -0
- 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 +278 -0
- package/src/scripts/second_brain_run.ts +341 -0
- package/src/scripts/second_brain_score.ts +186 -0
- package/src/scripts/session_memory_index.ts +94 -0
- package/src/scripts/skill_eval_coverage.ts +318 -0
- package/src/server/schemas/settings.ts +5 -2
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Versioned-cache lint (road-to-retrieval-substrate-hardening B5b).
|
|
4
|
+
*
|
|
5
|
+
* Every derived cache the suite writes MUST be safe to invalidate when its
|
|
6
|
+
* producer changes — otherwise a tool-version bump silently reads a
|
|
7
|
+
* stale-format cache and corrupts the read path. This lint makes that
|
|
8
|
+
* discipline structural: any cache-file path literal in `src/scripts/**`
|
|
9
|
+
* must EITHER carry a version namespace in the path (`v<N>`, a `${…version…}`
|
|
10
|
+
* interpolation, or a `_VERSION`/`schema_version` reference), OR carry an
|
|
11
|
+
* explicit invalidation justification comment (`cache-version:` /
|
|
12
|
+
* `cache-invalidation:`) within a few lines — an author's ack that the cache
|
|
13
|
+
* is invalidated by another mechanism (full overwrite, content hash, …).
|
|
14
|
+
*
|
|
15
|
+
* It is a FAILING gate (this roadmap introduces it), scoped tightly to
|
|
16
|
+
* cache-file SUFFIXES so it has near-zero false-positive surface: it fires on
|
|
17
|
+
* the derived caches later phases create (B2 index, B3 learning sidecar, B5a
|
|
18
|
+
* stat-index), not on directories or ordinary data files.
|
|
19
|
+
*
|
|
20
|
+
* Usage: lint_versioned_cache.ts [--dir <root>] [--format text|json] [--quiet]
|
|
21
|
+
* Exit codes: 0 = clean, 1 = usage error, 2 = violations found, 3 = internal.
|
|
22
|
+
*/
|
|
23
|
+
import * as fs from 'node:fs';
|
|
24
|
+
import * as path from 'node:path';
|
|
25
|
+
import process from 'node:process';
|
|
26
|
+
import { fileURLToPath } from 'node:url';
|
|
27
|
+
|
|
28
|
+
const PROG = 'lint_versioned_cache.ts';
|
|
29
|
+
|
|
30
|
+
/** Window (lines) around a cache-path literal scanned for a justification. */
|
|
31
|
+
export const JUSTIFY_WINDOW = 6;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* A path literal is a "derived cache file" when its basename matches one of
|
|
35
|
+
* these suffixes. Suffix-based (not dir-based) to keep the trigger precise —
|
|
36
|
+
* ordinary data/config JSON does not match.
|
|
37
|
+
*/
|
|
38
|
+
export const CACHE_SUFFIX_RE =
|
|
39
|
+
/(?:^|[\/'"`\-.])(?:[a-z0-9_\-.]*)(?:-index|\.index|-cache|\.cache|\.stat-index|-stat-index|\.agent-learning)\.json(?:['"`]|$)/i;
|
|
40
|
+
|
|
41
|
+
/** A version namespace present in the path literal itself. */
|
|
42
|
+
const VERSION_IN_PATH_RE = /(?:[\/_\-.]v\d+|\$\{[^}]*[Vv]ersion[^}]*\}|\$\{[^}]*VERSION[^}]*\})/;
|
|
43
|
+
|
|
44
|
+
/** An explicit author justification comment near the literal. */
|
|
45
|
+
const JUSTIFY_RE = /(?:cache-version:|cache-invalidation:)/i;
|
|
46
|
+
|
|
47
|
+
/** A string / template literal containing a cache-file path. */
|
|
48
|
+
const LITERAL_RE = /(['"`])((?:\\.|(?!\1).)*?)\1/g;
|
|
49
|
+
|
|
50
|
+
export interface Violation {
|
|
51
|
+
file: string;
|
|
52
|
+
line: number; // 1-indexed
|
|
53
|
+
literal: string;
|
|
54
|
+
message: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function listTsFiles(root: string): string[] {
|
|
58
|
+
const out: string[] = [];
|
|
59
|
+
const skip = new Set([
|
|
60
|
+
'node_modules',
|
|
61
|
+
'.git',
|
|
62
|
+
'dist',
|
|
63
|
+
'coverage',
|
|
64
|
+
'.turbo',
|
|
65
|
+
'.cache',
|
|
66
|
+
'__pycache__',
|
|
67
|
+
]);
|
|
68
|
+
const walk = (dir: string): void => {
|
|
69
|
+
let entries: fs.Dirent[];
|
|
70
|
+
try {
|
|
71
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
72
|
+
} catch {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
for (const e of entries) {
|
|
76
|
+
if (e.isDirectory()) {
|
|
77
|
+
if (!skip.has(e.name)) walk(path.join(dir, e.name));
|
|
78
|
+
} else if (e.isFile() && e.name.endsWith('.ts') && !e.name.endsWith('.d.ts')) {
|
|
79
|
+
out.push(path.join(dir, e.name));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
walk(root);
|
|
84
|
+
return out.sort();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function _isCachePathLiteral(value: string): boolean {
|
|
88
|
+
// Reset lastIndex — CACHE_SUFFIX_RE has no /g flag, so test() is safe.
|
|
89
|
+
return CACHE_SUFFIX_RE.test(value);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function scanFile(file: string, source: string): Violation[] {
|
|
93
|
+
const lines = source.split('\n');
|
|
94
|
+
const violations: Violation[] = [];
|
|
95
|
+
for (let i = 0; i < lines.length; i++) {
|
|
96
|
+
const text = lines[i] as string;
|
|
97
|
+
LITERAL_RE.lastIndex = 0;
|
|
98
|
+
let m: RegExpExecArray | null;
|
|
99
|
+
while ((m = LITERAL_RE.exec(text)) !== null) {
|
|
100
|
+
const value = m[2] as string;
|
|
101
|
+
if (!_isCachePathLiteral(value)) continue;
|
|
102
|
+
if (VERSION_IN_PATH_RE.test(value)) continue; // versioned in the path itself
|
|
103
|
+
|
|
104
|
+
// Look for a justification comment in the window around the literal.
|
|
105
|
+
const from = Math.max(0, i - JUSTIFY_WINDOW);
|
|
106
|
+
const to = Math.min(lines.length - 1, i + JUSTIFY_WINDOW);
|
|
107
|
+
let justified = false;
|
|
108
|
+
for (let j = from; j <= to; j++) {
|
|
109
|
+
if (JUSTIFY_RE.test(lines[j] as string)) {
|
|
110
|
+
justified = true;
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (justified) continue;
|
|
115
|
+
|
|
116
|
+
violations.push({
|
|
117
|
+
file,
|
|
118
|
+
line: i + 1,
|
|
119
|
+
literal: value,
|
|
120
|
+
message:
|
|
121
|
+
'derived cache without a version namespace — add a `v<N>` (or ' +
|
|
122
|
+
'`${…version…}`) segment to the path, or an inline ' +
|
|
123
|
+
'`// cache-invalidation: <why no version is needed>` comment',
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return violations;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function runChecks(root: string): Violation[] {
|
|
131
|
+
const violations: Violation[] = [];
|
|
132
|
+
for (const file of listTsFiles(root)) {
|
|
133
|
+
// Never lint this file itself — its regexes contain the very suffixes
|
|
134
|
+
// it hunts for.
|
|
135
|
+
if (path.basename(file) === 'lint_versioned_cache.ts') continue;
|
|
136
|
+
let source: string;
|
|
137
|
+
try {
|
|
138
|
+
source = fs.readFileSync(file, 'utf-8');
|
|
139
|
+
} catch {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
violations.push(...scanFile(file, source));
|
|
143
|
+
}
|
|
144
|
+
return violations;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function main(argv: string[]): number {
|
|
148
|
+
let dir = 'src/scripts';
|
|
149
|
+
let format: 'text' | 'json' = 'text';
|
|
150
|
+
let quiet = false;
|
|
151
|
+
for (let i = 0; i < argv.length; i++) {
|
|
152
|
+
const a = argv[i] as string;
|
|
153
|
+
if (a === '--dir') {
|
|
154
|
+
dir = argv[++i] ?? dir;
|
|
155
|
+
} else if (a.startsWith('--dir=')) {
|
|
156
|
+
dir = a.slice('--dir='.length);
|
|
157
|
+
} else if (a === '--format') {
|
|
158
|
+
format = (argv[++i] as 'text' | 'json') ?? 'text';
|
|
159
|
+
} else if (a === '--quiet') {
|
|
160
|
+
quiet = true;
|
|
161
|
+
} else if (a === '-h' || a === '--help') {
|
|
162
|
+
process.stdout.write(`usage: ${PROG} [--dir <root>] [--format text|json] [--quiet]\n`);
|
|
163
|
+
return 0;
|
|
164
|
+
} else {
|
|
165
|
+
process.stderr.write(`${PROG}: error: unknown argument ${a}\n`);
|
|
166
|
+
return 1;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
let violations: Violation[];
|
|
171
|
+
try {
|
|
172
|
+
violations = runChecks(path.resolve(dir));
|
|
173
|
+
} catch (exc) {
|
|
174
|
+
process.stderr.write(`${PROG}: internal error: ${String(exc)}\n`);
|
|
175
|
+
return 3;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (format === 'json') {
|
|
179
|
+
process.stdout.write(JSON.stringify({ violations }, null, 2) + '\n');
|
|
180
|
+
return violations.length > 0 ? 2 : 0;
|
|
181
|
+
}
|
|
182
|
+
if (violations.length === 0) {
|
|
183
|
+
if (!quiet) process.stdout.write(`${PROG}: no unversioned derived caches\n`);
|
|
184
|
+
return 0;
|
|
185
|
+
}
|
|
186
|
+
for (const v of violations) {
|
|
187
|
+
process.stdout.write(`❌ ${v.file}:${v.line} — cache '${v.literal}': ${v.message}\n`);
|
|
188
|
+
}
|
|
189
|
+
process.stdout.write(`${PROG}: ${violations.length} unversioned derived cache(s)\n`);
|
|
190
|
+
return 2;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (fileURLToPath(import.meta.url) === path.resolve(process.argv[1] ?? '')) {
|
|
194
|
+
process.exit(main(process.argv.slice(2)));
|
|
195
|
+
}
|
|
@@ -50,14 +50,18 @@
|
|
|
50
50
|
"last": {"type": "integer", "description": "Return only the most recent N entries, after other filters apply.", "minimum": 1},
|
|
51
51
|
"session": {"type": "string", "description": "Filter to a single 16-char session id."},
|
|
52
52
|
"entry_type": {"type": "string", "description": "Filter by the `t` tag (e.g. note, decision, phase)."},
|
|
53
|
-
"path": {"type": "string", "description": "Optional history-file path override; defaults to the standard chat-history location under the project root."}
|
|
53
|
+
"path": {"type": "string", "description": "Optional history-file path override; defaults to the standard chat-history location under the project root."},
|
|
54
|
+
"around": {"type": "integer", "minimum": 0, "description": "Timeline anchor: return the entries around this ref (from a detail:\"index\" row) instead of the filtered list — depth_before/depth_after neighbours plus the anchor. Refs are within-file ordinals; rotation invalidates them."},
|
|
55
|
+
"depth_before": {"type": "integer", "minimum": 0, "default": 3, "description": "Neighbours before the anchor (with `around`). Defaults to 3."},
|
|
56
|
+
"depth_after": {"type": "integer", "minimum": 0, "default": 3, "description": "Neighbours after the anchor (with `around`). Defaults to 3."},
|
|
57
|
+
"detail": {"type": "string", "enum": ["index", "full"], "default": "full", "description": "'index' returns compact rows (ref, t tag, ~100-char preview, tokens_estimate) instead of full entries — scan first, then re-read the refs you need via `around` or a narrowed filter. 'full' (default) returns complete entries."}
|
|
54
58
|
},
|
|
55
59
|
"additionalProperties": false
|
|
56
60
|
}
|
|
57
61
|
},
|
|
58
62
|
{
|
|
59
63
|
"name": "memory_lookup",
|
|
60
|
-
"description": "Retrieve engineering-memory entries for one or more memory types, optionally narrowed to specific anchor paths. Use before editing a security-sensitive or historically buggy file to surface prior incidents, ownership, and patterns tied to it. Reads `agents/memory/<type>/*.yml` plus the `agents/memory/intake/*.jsonl` signal log. Read-only. Returns the v1 retrieval envelope: a `status` field plus per-type `slices` carrying the matched entries.",
|
|
64
|
+
"description": "Retrieve engineering-memory entries for one or more memory types, optionally narrowed to specific anchor paths. Use before editing a security-sensitive or historically buggy file to surface prior incidents, ownership, and patterns tied to it. WORKFLOW: call with detail:\"index\" FIRST — each row carries id, title and tokens_estimate (the cost of fetching it) — then fetch full bodies via memory_get ONLY for the ids you will actually use, batching multiple ids into one call. Reads `agents/memory/<type>/*.yml` plus the `agents/memory/intake/*.jsonl` signal log. Read-only. Returns the v1 retrieval envelope: a `status` field plus per-type `slices` carrying the matched entries.",
|
|
61
65
|
"side_effect": "ro",
|
|
62
66
|
"implemented_on": ["stdio"],
|
|
63
67
|
"input_schema": {
|
|
@@ -65,12 +69,28 @@
|
|
|
65
69
|
"properties": {
|
|
66
70
|
"types": {"type": "array", "items": {"type": "string"}, "minItems": 1, "description": "Memory types to scan, e.g. `historical-patterns`, `incident-learnings`, `ownership`. At least one required."},
|
|
67
71
|
"keys": {"type": "array", "items": {"type": "string"}, "description": "Optional anchor paths or globs to match entries against (e.g. a file you are about to edit)."},
|
|
68
|
-
"limit": {"type": "integer", "minimum": 1, "default": 5, "description": "Maximum entries to return per type. Defaults to 5."}
|
|
72
|
+
"limit": {"type": "integer", "minimum": 1, "default": 5, "description": "Maximum entries to return per type. Defaults to 5."},
|
|
73
|
+
"detail": {"type": "string", "enum": ["index", "full"], "default": "full", "description": "'index' returns compact priced rows (id, title, tokens_estimate) instead of full bodies — call this first, then memory_get the ids you need. 'full' (default) returns complete entries."},
|
|
74
|
+
"token_budget": {"type": "integer", "minimum": 1, "description": "Optional token budget. When set, entries are rendered as one-line compact rows (id, type, confidence, `line`) and the row set is hard-cut at token_budget × 4 chars; omitted hits appear as a top-level `truncation` hint naming a concrete next step. Absent → the envelope is unchanged."}
|
|
69
75
|
},
|
|
70
76
|
"required": ["types"],
|
|
71
77
|
"additionalProperties": false
|
|
72
78
|
}
|
|
73
79
|
},
|
|
80
|
+
{
|
|
81
|
+
"name": "memory_get",
|
|
82
|
+
"description": "Batch-fetch FULL memory entries by id — the second half of the index-first retrieval workflow. Call memory_lookup with detail:\"index\" first, pick the ids whose title/tokens_estimate justify the fetch, then fetch them here in ONE batched call. Unknown ids are reported per-id (ids[<id>]=\"unknown\"), never failing the batch. Read-only.",
|
|
83
|
+
"side_effect": "ro",
|
|
84
|
+
"implemented_on": ["stdio"],
|
|
85
|
+
"input_schema": {
|
|
86
|
+
"type": "object",
|
|
87
|
+
"properties": {
|
|
88
|
+
"ids": {"type": "array", "items": {"type": "string"}, "minItems": 1, "description": "Entry ids to fetch (from a detail:\"index\" lookup)."}
|
|
89
|
+
},
|
|
90
|
+
"required": ["ids"],
|
|
91
|
+
"additionalProperties": false
|
|
92
|
+
}
|
|
93
|
+
},
|
|
74
94
|
{
|
|
75
95
|
"name": "memory_signal",
|
|
76
96
|
"description": "Record an engineering-memory signal — a short, anchored observation such as a recurring bug pattern or an ownership note — to the monthly intake log `agents/memory/intake/signals-YYYY-MM.jsonl`. Use to capture a learning tied to a specific file so future `memory_lookup` calls surface it. Appends to the filesystem and is rate-limited per `(type, path)` within a rolling window. Returns the recorded signal.",
|
|
@@ -38,7 +38,10 @@ import {
|
|
|
38
38
|
SCHEMA_VERSION as _CH_SCHEMA_VERSION,
|
|
39
39
|
append as _chAppendImpl,
|
|
40
40
|
init as _chInitImpl,
|
|
41
|
+
history_index_row as _chHistoryIndexRow,
|
|
41
42
|
read_entries as _chReadEntriesImpl,
|
|
43
|
+
read_entries_with_refs as _chReadEntriesWithRefs,
|
|
44
|
+
slice_around as _chSliceAround,
|
|
42
45
|
read_header as _chReadHeaderImpl,
|
|
43
46
|
} from '../chat_history.js';
|
|
44
47
|
import {
|
|
@@ -364,10 +367,63 @@ async function _chatHistoryReadHandler(
|
|
|
364
367
|
throw new Error("'entry_type' must be a string when provided");
|
|
365
368
|
}
|
|
366
369
|
|
|
370
|
+
const around = args.around;
|
|
371
|
+
if (
|
|
372
|
+
around !== undefined &&
|
|
373
|
+
around !== null &&
|
|
374
|
+
(typeof around !== 'number' || !Number.isInteger(around) || around < 0)
|
|
375
|
+
) {
|
|
376
|
+
throw new Error("'around' must be a non-negative integer when provided");
|
|
377
|
+
}
|
|
378
|
+
const depthBefore = args.depth_before ?? 3;
|
|
379
|
+
const depthAfter = args.depth_after ?? 3;
|
|
380
|
+
for (const [name, v] of [
|
|
381
|
+
['depth_before', depthBefore],
|
|
382
|
+
['depth_after', depthAfter],
|
|
383
|
+
] as const) {
|
|
384
|
+
if (typeof v !== 'number' || !Number.isInteger(v) || v < 0) {
|
|
385
|
+
throw new Error(`'${name}' must be a non-negative integer`);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
const detail = args.detail ?? 'full';
|
|
389
|
+
if (detail !== 'full' && detail !== 'index') {
|
|
390
|
+
throw new Error("'detail' must be 'index' or 'full'");
|
|
391
|
+
}
|
|
392
|
+
|
|
367
393
|
if (!fs.existsSync(target)) {
|
|
368
394
|
return { path: target, entries: [], count: 0 };
|
|
369
395
|
}
|
|
370
396
|
|
|
397
|
+
// Timeline anchor path (road-to-memory-retrieval-economy Phase 3):
|
|
398
|
+
// refs index the FULL chronological list, so `around` ignores the
|
|
399
|
+
// session/entry_type/last filters by contract.
|
|
400
|
+
if (around !== undefined && around !== null) {
|
|
401
|
+
const pairs = _chSliceAround(
|
|
402
|
+
target,
|
|
403
|
+
around as number,
|
|
404
|
+
depthBefore as number,
|
|
405
|
+
depthAfter as number,
|
|
406
|
+
);
|
|
407
|
+
const entries =
|
|
408
|
+
detail === 'index'
|
|
409
|
+
? pairs.map((p) => _chHistoryIndexRow(p))
|
|
410
|
+
: pairs.map((p) => ({ ref: p.ref, ...p.entry }));
|
|
411
|
+
return { path: target, entries, count: entries.length, anchor: around };
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (detail === 'index') {
|
|
415
|
+
let pairs = _chReadEntriesWithRefs({
|
|
416
|
+
path: target,
|
|
417
|
+
last: (last ?? null) as number | null,
|
|
418
|
+
session: (session ?? null) as string | null,
|
|
419
|
+
});
|
|
420
|
+
if (entryType) {
|
|
421
|
+
pairs = pairs.filter((p) => p.entry.t === entryType);
|
|
422
|
+
}
|
|
423
|
+
const entries = pairs.map((p) => _chHistoryIndexRow(p));
|
|
424
|
+
return { path: target, entries, count: entries.length };
|
|
425
|
+
}
|
|
426
|
+
|
|
371
427
|
let entries = _chReadEntries(
|
|
372
428
|
target,
|
|
373
429
|
(last ?? null) as number | null,
|
|
@@ -416,6 +472,23 @@ async function _memoryLookupHandler(
|
|
|
416
472
|
// Mirror the Python `os.chdir(consumer_root)` scoping: memory_lookup
|
|
417
473
|
// resolves `agents/memory` relative to CWD, so the consumer root is
|
|
418
474
|
// applied via a chdir window that is always restored.
|
|
475
|
+
const detailRaw = args.detail ?? 'full';
|
|
476
|
+
if (detailRaw !== 'full' && detailRaw !== 'index') {
|
|
477
|
+
throw new Error("'detail' must be 'index' or 'full'");
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// B1 (road-to-retrieval-substrate-hardening): optional token budget →
|
|
481
|
+
// compact one-line rows, hard-cut with a truncation hint. Absent → the
|
|
482
|
+
// envelope is unchanged (v1 default-full contract holds).
|
|
483
|
+
const opts: { detail: 'full' | 'index'; token_budget?: number } = { detail: detailRaw };
|
|
484
|
+
if (args.token_budget !== undefined) {
|
|
485
|
+
const tb = args.token_budget;
|
|
486
|
+
if (typeof tb !== 'number' || !Number.isInteger(tb) || typeof tb === 'boolean' || tb < 1) {
|
|
487
|
+
throw new Error("'token_budget' must be a positive integer");
|
|
488
|
+
}
|
|
489
|
+
opts.token_budget = tb;
|
|
490
|
+
}
|
|
491
|
+
|
|
419
492
|
const prevCwd = process.cwd();
|
|
420
493
|
let envelope: Record<string, unknown>;
|
|
421
494
|
try {
|
|
@@ -424,6 +497,7 @@ async function _memoryLookupHandler(
|
|
|
424
497
|
[...(types as string[])],
|
|
425
498
|
[...(keys as string[])],
|
|
426
499
|
limitRaw,
|
|
500
|
+
opts,
|
|
427
501
|
);
|
|
428
502
|
} finally {
|
|
429
503
|
process.chdir(prevCwd);
|
|
@@ -431,6 +505,30 @@ async function _memoryLookupHandler(
|
|
|
431
505
|
return envelope;
|
|
432
506
|
}
|
|
433
507
|
|
|
508
|
+
/**
|
|
509
|
+
* Batch full-entry fetch by ID (road-to-memory-retrieval-economy Phase 1).
|
|
510
|
+
* The second half of the index→fetch economy: `memory_lookup` with
|
|
511
|
+
* `detail: 'index'` returns priced rows; this fetches ONLY the bodies the
|
|
512
|
+
* caller actually needs. Unknown IDs degrade per-ID, never the batch.
|
|
513
|
+
*/
|
|
514
|
+
async function _memoryGetHandler(
|
|
515
|
+
args: Record<string, unknown>,
|
|
516
|
+
consumerRoot: string,
|
|
517
|
+
): Promise<Record<string, unknown>> {
|
|
518
|
+
const memoryLookup = await import('../memory_lookup.js');
|
|
519
|
+
const ids = args.ids;
|
|
520
|
+
if (!Array.isArray(ids) || ids.length === 0 || !ids.every((i) => typeof i === 'string')) {
|
|
521
|
+
throw new Error("'ids' must be a non-empty list of strings");
|
|
522
|
+
}
|
|
523
|
+
const prevCwd = process.cwd();
|
|
524
|
+
try {
|
|
525
|
+
process.chdir(consumerRoot);
|
|
526
|
+
return memoryLookup.memory_get_v1([...(ids as string[])]);
|
|
527
|
+
} finally {
|
|
528
|
+
process.chdir(prevCwd);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
434
532
|
/** Phase 3 L2 — surface `scripts/memory_status.status()` as JSON. */
|
|
435
533
|
async function _memoryStatusHandler(
|
|
436
534
|
_args: Record<string, unknown>,
|
|
@@ -1225,6 +1323,40 @@ export const ALLOWLIST: Record<string, BuiltinTool> = {
|
|
|
1225
1323
|
'the standard chat-history location under the ' +
|
|
1226
1324
|
'project root.',
|
|
1227
1325
|
},
|
|
1326
|
+
around: {
|
|
1327
|
+
type: 'integer',
|
|
1328
|
+
minimum: 0,
|
|
1329
|
+
description:
|
|
1330
|
+
'Timeline anchor: return the entries around this ' +
|
|
1331
|
+
'ref (from a detail:"index" row) instead of the ' +
|
|
1332
|
+
'filtered list — depth_before/depth_after ' +
|
|
1333
|
+
'neighbours plus the anchor. Refs are within-file ' +
|
|
1334
|
+
'ordinals; rotation invalidates them.',
|
|
1335
|
+
},
|
|
1336
|
+
depth_before: {
|
|
1337
|
+
type: 'integer',
|
|
1338
|
+
minimum: 0,
|
|
1339
|
+
default: 3,
|
|
1340
|
+
description: 'Neighbours before the anchor (with `around`). Defaults to 3.',
|
|
1341
|
+
},
|
|
1342
|
+
depth_after: {
|
|
1343
|
+
type: 'integer',
|
|
1344
|
+
minimum: 0,
|
|
1345
|
+
default: 3,
|
|
1346
|
+
description: 'Neighbours after the anchor (with `around`). Defaults to 3.',
|
|
1347
|
+
},
|
|
1348
|
+
detail: {
|
|
1349
|
+
type: 'string',
|
|
1350
|
+
enum: ['index', 'full'],
|
|
1351
|
+
default: 'full',
|
|
1352
|
+
description:
|
|
1353
|
+
"'index' returns compact rows (ref, t tag, ~100-char " +
|
|
1354
|
+
'preview, tokens_estimate) instead of full entries — ' +
|
|
1355
|
+
'scan first, then re-read the refs you need via ' +
|
|
1356
|
+
'`around` or a narrowed filter. History entries are ' +
|
|
1357
|
+
"large, so index mode pays off here. 'full' " +
|
|
1358
|
+
'(default) returns complete entries.',
|
|
1359
|
+
},
|
|
1228
1360
|
},
|
|
1229
1361
|
additionalProperties: false,
|
|
1230
1362
|
},
|
|
@@ -1237,7 +1369,11 @@ export const ALLOWLIST: Record<string, BuiltinTool> = {
|
|
|
1237
1369
|
'types, optionally narrowed to specific anchor paths. Use ' +
|
|
1238
1370
|
'before editing a security-sensitive or historically buggy ' +
|
|
1239
1371
|
'file to surface prior incidents, ownership, and patterns ' +
|
|
1240
|
-
'tied to it.
|
|
1372
|
+
'tied to it. WORKFLOW: call with detail:"index" FIRST — each ' +
|
|
1373
|
+
'row carries id, title and tokens_estimate (the cost of ' +
|
|
1374
|
+
'fetching it) — then fetch full bodies via memory_get ONLY ' +
|
|
1375
|
+
'for the ids you will actually use, batching multiple ids ' +
|
|
1376
|
+
'into one call. Reads `agents/memory/<type>/*.yml` plus the ' +
|
|
1241
1377
|
'`agents/memory/intake/*.jsonl` signal log. Read-only. ' +
|
|
1242
1378
|
'Returns the v1 retrieval envelope: a `status` field plus ' +
|
|
1243
1379
|
'per-type `slices` carrying the matched entries.',
|
|
@@ -1266,12 +1402,57 @@ export const ALLOWLIST: Record<string, BuiltinTool> = {
|
|
|
1266
1402
|
default: 5,
|
|
1267
1403
|
description: 'Maximum entries to return per type. Defaults to 5.',
|
|
1268
1404
|
},
|
|
1405
|
+
detail: {
|
|
1406
|
+
type: 'string',
|
|
1407
|
+
enum: ['index', 'full'],
|
|
1408
|
+
default: 'full',
|
|
1409
|
+
description:
|
|
1410
|
+
"'index' returns compact priced rows (id, title, " +
|
|
1411
|
+
"tokens_estimate) instead of full bodies — call " +
|
|
1412
|
+
"this first, then memory_get the ids you need. " +
|
|
1413
|
+
"'full' (default) returns complete entries.",
|
|
1414
|
+
},
|
|
1415
|
+
token_budget: {
|
|
1416
|
+
type: 'integer',
|
|
1417
|
+
minimum: 1,
|
|
1418
|
+
description:
|
|
1419
|
+
'Optional token budget. When set, entries are ' +
|
|
1420
|
+
'rendered as one-line compact rows (id, type, ' +
|
|
1421
|
+
'confidence, `line`) and the row set is hard-cut at ' +
|
|
1422
|
+
'token_budget × 4 chars; omitted hits appear as a ' +
|
|
1423
|
+
'top-level `truncation` hint naming a concrete next ' +
|
|
1424
|
+
'step. Absent → the envelope is unchanged.',
|
|
1425
|
+
},
|
|
1269
1426
|
},
|
|
1270
1427
|
required: ['types'],
|
|
1271
1428
|
additionalProperties: false,
|
|
1272
1429
|
},
|
|
1273
1430
|
handler: _memoryLookupHandler,
|
|
1274
1431
|
},
|
|
1432
|
+
memory_get: {
|
|
1433
|
+
name: 'memory_get',
|
|
1434
|
+
description:
|
|
1435
|
+
'Batch-fetch FULL memory entries by id — the second half of ' +
|
|
1436
|
+
'the index-first retrieval workflow. Call memory_lookup with ' +
|
|
1437
|
+
'detail:"index" first, pick the ids whose title/tokens_estimate ' +
|
|
1438
|
+
'justify the fetch, then fetch them here in ONE batched call. ' +
|
|
1439
|
+
'Unknown ids are reported per-id (ids[<id>]="unknown"), never ' +
|
|
1440
|
+
'failing the batch. Read-only.',
|
|
1441
|
+
input_schema: {
|
|
1442
|
+
type: 'object',
|
|
1443
|
+
properties: {
|
|
1444
|
+
ids: {
|
|
1445
|
+
type: 'array',
|
|
1446
|
+
items: { type: 'string' },
|
|
1447
|
+
minItems: 1,
|
|
1448
|
+
description: 'Entry ids to fetch (from a detail:"index" lookup).',
|
|
1449
|
+
},
|
|
1450
|
+
},
|
|
1451
|
+
required: ['ids'],
|
|
1452
|
+
additionalProperties: false,
|
|
1453
|
+
},
|
|
1454
|
+
handler: _memoryGetHandler,
|
|
1455
|
+
},
|
|
1275
1456
|
memory_status: {
|
|
1276
1457
|
name: 'memory_status',
|
|
1277
1458
|
description:
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Ranking-lift measurement for the hand-rolled lexical index
|
|
4
|
+
* (road-to-retrieval-substrate-hardening B2, deterministic + free).
|
|
5
|
+
*
|
|
6
|
+
* The gate on shipping the index (roadmap Phase 2): it must measurably rank
|
|
7
|
+
* BETTER than the current `_score` bucket scorer, else honest-null. This
|
|
8
|
+
* script runs both scorers over the SAME retrieval-precision corpus + store
|
|
9
|
+
* (internal/bench/second-brain/retrieval-store/) and reports, per task and in
|
|
10
|
+
* aggregate:
|
|
11
|
+
*
|
|
12
|
+
* - mean top tie-set size — how many entries share the TOP score. `_score`
|
|
13
|
+
* gives 0.8 to any keyword match, so the needed entry ties with its
|
|
14
|
+
* confusers (the "recalls but does not rank" finding, mean 3.3). A good
|
|
15
|
+
* ranker drives this toward 1.
|
|
16
|
+
* - precision@1 / @k — was the NEEDED entry actually first / in the top-k.
|
|
17
|
+
*
|
|
18
|
+
* No model, no spend: pure retrieval scoring. Exit 0 ok / 1 error / 2 usage.
|
|
19
|
+
*
|
|
20
|
+
* Usage: measure_lexical_ranking.ts [--write] [--format text|json]
|
|
21
|
+
* --write persist the report to internal/bench/reports/lexical-ranking.json
|
|
22
|
+
*/
|
|
23
|
+
import * as fs from 'node:fs';
|
|
24
|
+
import * as path from 'node:path';
|
|
25
|
+
import { fileURLToPath } from 'node:url';
|
|
26
|
+
|
|
27
|
+
import * as YAML from 'yaml';
|
|
28
|
+
|
|
29
|
+
import { LexicalIndex } from './_lib/lexical_index.js';
|
|
30
|
+
import { _score, _setIntakeRoot, _setKnowledgeRoot, _setMemoryRoot, retrieve } from './memory_lookup.js';
|
|
31
|
+
|
|
32
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
33
|
+
export const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
34
|
+
export const STORE_DIR = path.join(REPO_ROOT, 'internal', 'bench', 'second-brain', 'retrieval-store');
|
|
35
|
+
export const CORPUS = path.join(STORE_DIR, 'retrieval-corpus.yml');
|
|
36
|
+
export const REPORT = path.join(REPO_ROOT, 'internal', 'bench', 'reports', 'lexical-ranking.json');
|
|
37
|
+
|
|
38
|
+
interface Task {
|
|
39
|
+
id: string;
|
|
40
|
+
query: string[];
|
|
41
|
+
needed: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface TaskResult {
|
|
45
|
+
id: string;
|
|
46
|
+
needed: string;
|
|
47
|
+
baseline: { rank: number; tieSet: number };
|
|
48
|
+
index: { rank: number; tieSet: number };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface RankingSummary {
|
|
52
|
+
tasks: number;
|
|
53
|
+
k: number;
|
|
54
|
+
baseline_mean_tie_set: number;
|
|
55
|
+
index_mean_tie_set: number;
|
|
56
|
+
baseline_precision_at_1: number;
|
|
57
|
+
index_precision_at_1: number;
|
|
58
|
+
baseline_precision_at_k: number;
|
|
59
|
+
index_precision_at_k: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Text an entry contributes to the index — the fields `_score` also reads. */
|
|
63
|
+
function entryText(entry: Record<string, unknown>): string {
|
|
64
|
+
const parts: string[] = [];
|
|
65
|
+
for (const f of ['path', 'key', 'symptom', 'feature', 'rule', 'body']) {
|
|
66
|
+
const v = entry[f];
|
|
67
|
+
if (typeof v === 'string') parts.push(v);
|
|
68
|
+
else if (Array.isArray(v)) for (const x of v) parts.push(String(x));
|
|
69
|
+
}
|
|
70
|
+
return parts.join(' | ');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Top tie-set size + rank of the needed id in a sorted (id, score) list. */
|
|
74
|
+
function diagnose(ranked: Array<{ id: string; score: number }>, needed: string): { rank: number; tieSet: number } {
|
|
75
|
+
if (ranked.length === 0) return { rank: -1, tieSet: 0 };
|
|
76
|
+
const top = ranked[0]?.score ?? 0;
|
|
77
|
+
const tieSet = ranked.filter((r) => r.score === top).length;
|
|
78
|
+
const rank = ranked.findIndex((r) => r.id === needed);
|
|
79
|
+
return { rank, tieSet };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function measure(): { tasks: TaskResult[]; summary: RankingSummary } {
|
|
83
|
+
_setMemoryRoot(STORE_DIR);
|
|
84
|
+
_setKnowledgeRoot(path.join(STORE_DIR, 'knowledge-none'));
|
|
85
|
+
_setIntakeRoot(path.join(STORE_DIR, 'intake-none'));
|
|
86
|
+
|
|
87
|
+
const doc = YAML.parse(fs.readFileSync(CORPUS, 'utf-8')) as { type: string; k: number; tasks: Task[] };
|
|
88
|
+
const type = doc.type;
|
|
89
|
+
const k = doc.k;
|
|
90
|
+
const results: TaskResult[] = [];
|
|
91
|
+
|
|
92
|
+
for (const task of doc.tasks) {
|
|
93
|
+
// Shared universe: every store entry `_score` considers relevant.
|
|
94
|
+
const hits = retrieve([type], task.query, 100);
|
|
95
|
+
|
|
96
|
+
// Baseline — rank by the current bucket scorer, ties broken by the
|
|
97
|
+
// order `retrieve` already returns (store order), exactly as today.
|
|
98
|
+
const baselineRanked = hits.map((h) => ({ id: h.id, score: h.score }));
|
|
99
|
+
const baseline = diagnose(baselineRanked, task.needed);
|
|
100
|
+
|
|
101
|
+
// Index — BM25 over the same entries' text.
|
|
102
|
+
const index = new LexicalIndex(hits.map((h) => ({ id: h.id, text: entryText(h.entry) })));
|
|
103
|
+
const indexRanked = index.rank(task.query);
|
|
104
|
+
const idx = diagnose(indexRanked, task.needed);
|
|
105
|
+
|
|
106
|
+
results.push({ id: task.id, needed: task.needed, baseline, index: idx });
|
|
107
|
+
void _score; // referenced for provenance — retrieve() applies it internally
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const n = results.length;
|
|
111
|
+
const mean = (sel: (r: TaskResult) => number): number =>
|
|
112
|
+
n === 0 ? 0 : Math.round((results.reduce((a, r) => a + sel(r), 0) / n) * 1000) / 1000;
|
|
113
|
+
const precAt = (sel: (r: TaskResult) => number, kk: number): number =>
|
|
114
|
+
n === 0 ? 0 : Math.round((results.filter((r) => sel(r) >= 0 && sel(r) < kk).length / n) * 1000) / 1000;
|
|
115
|
+
|
|
116
|
+
const summary = {
|
|
117
|
+
tasks: n,
|
|
118
|
+
k,
|
|
119
|
+
baseline_mean_tie_set: mean((r) => r.baseline.tieSet),
|
|
120
|
+
index_mean_tie_set: mean((r) => r.index.tieSet),
|
|
121
|
+
baseline_precision_at_1: precAt((r) => r.baseline.rank, 1),
|
|
122
|
+
index_precision_at_1: precAt((r) => r.index.rank, 1),
|
|
123
|
+
baseline_precision_at_k: precAt((r) => r.baseline.rank, k),
|
|
124
|
+
index_precision_at_k: precAt((r) => r.index.rank, k),
|
|
125
|
+
};
|
|
126
|
+
return { tasks: results, summary };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function main(argv: string[]): number {
|
|
130
|
+
let write = false;
|
|
131
|
+
let format: 'text' | 'json' = 'text';
|
|
132
|
+
for (let i = 0; i < argv.length; i++) {
|
|
133
|
+
const a = argv[i] as string;
|
|
134
|
+
if (a === '--write') write = true;
|
|
135
|
+
else if (a === '--format') format = (argv[++i] as 'text' | 'json') ?? 'text';
|
|
136
|
+
else if (a === '-h' || a === '--help') {
|
|
137
|
+
process.stdout.write('usage: measure_lexical_ranking.ts [--write] [--format text|json]\n');
|
|
138
|
+
return 0;
|
|
139
|
+
} else {
|
|
140
|
+
process.stderr.write(`measure_lexical_ranking: unknown argument ${a}\n`);
|
|
141
|
+
return 2;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
let out: { tasks: TaskResult[]; summary: RankingSummary };
|
|
146
|
+
try {
|
|
147
|
+
out = measure();
|
|
148
|
+
} catch (exc) {
|
|
149
|
+
process.stderr.write(`measure_lexical_ranking: error: ${String(exc)}\n`);
|
|
150
|
+
return 1;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (write) {
|
|
154
|
+
fs.mkdirSync(path.dirname(REPORT), { recursive: true });
|
|
155
|
+
fs.writeFileSync(REPORT, JSON.stringify({ schema_version: 1, ...out }, null, 2) + '\n');
|
|
156
|
+
}
|
|
157
|
+
if (format === 'json') {
|
|
158
|
+
process.stdout.write(JSON.stringify(out, null, 2) + '\n');
|
|
159
|
+
return 0;
|
|
160
|
+
}
|
|
161
|
+
const s = out.summary;
|
|
162
|
+
process.stdout.write(
|
|
163
|
+
`lexical-ranking (${s.tasks} tasks, k=${s.k}):\n` +
|
|
164
|
+
` mean top tie-set baseline=${s.baseline_mean_tie_set} index=${s.index_mean_tie_set}\n` +
|
|
165
|
+
` precision@1 baseline=${s.baseline_precision_at_1} index=${s.index_precision_at_1}\n` +
|
|
166
|
+
` precision@${s.k} baseline=${s.baseline_precision_at_k} index=${s.index_precision_at_k}\n`,
|
|
167
|
+
);
|
|
168
|
+
return 0;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (fileURLToPath(import.meta.url) === path.resolve(process.argv[1] ?? '')) {
|
|
172
|
+
process.exit(main(process.argv.slice(2)));
|
|
173
|
+
}
|
|
Binary file
|