@treeseed/dsh-chapters 0.1.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/LICENSE +14 -0
- package/README.md +644 -0
- package/cordis.patch.yml +88 -0
- package/docs/architecture.md +460 -0
- package/docs/contract.md +375 -0
- package/docs/development.md +437 -0
- package/docs/host-compaction-seam.md +117 -0
- package/docs/knowledge-repo.md +730 -0
- package/docs/provider.md +98 -0
- package/docs/verify.md +200 -0
- package/lib/archive.d.ts +88 -0
- package/lib/archive.js +114 -0
- package/lib/archive.js.map +1 -0
- package/lib/arrival.d.ts +23 -0
- package/lib/arrival.js +86 -0
- package/lib/arrival.js.map +1 -0
- package/lib/artifact-query.d.ts +53 -0
- package/lib/artifact-query.js +130 -0
- package/lib/artifact-query.js.map +1 -0
- package/lib/client.js +234 -0
- package/lib/client.js.map +1 -0
- package/lib/commands.d.ts +28 -0
- package/lib/commands.js +305 -0
- package/lib/commands.js.map +1 -0
- package/lib/compose.d.ts +30 -0
- package/lib/compose.js +131 -0
- package/lib/compose.js.map +1 -0
- package/lib/continue-core.d.ts +153 -0
- package/lib/continue-core.js +252 -0
- package/lib/continue-core.js.map +1 -0
- package/lib/engine-core.d.ts +219 -0
- package/lib/engine-core.js +405 -0
- package/lib/engine-core.js.map +1 -0
- package/lib/engine.d.ts +211 -0
- package/lib/engine.js +571 -0
- package/lib/engine.js.map +1 -0
- package/lib/enrich-queue.d.ts +66 -0
- package/lib/enrich-queue.js +100 -0
- package/lib/enrich-queue.js.map +1 -0
- package/lib/enrich-store.d.ts +31 -0
- package/lib/enrich-store.js +103 -0
- package/lib/enrich-store.js.map +1 -0
- package/lib/enrich-wire.d.ts +73 -0
- package/lib/enrich-wire.js +209 -0
- package/lib/enrich-wire.js.map +1 -0
- package/lib/enrich.d.ts +73 -0
- package/lib/enrich.js +150 -0
- package/lib/enrich.js.map +1 -0
- package/lib/gitops.d.ts +107 -0
- package/lib/gitops.js +539 -0
- package/lib/gitops.js.map +1 -0
- package/lib/index.d.ts +81 -0
- package/lib/index.js +305 -0
- package/lib/index.js.map +1 -0
- package/lib/indexing.d.ts +85 -0
- package/lib/indexing.js +212 -0
- package/lib/indexing.js.map +1 -0
- package/lib/injections.d.ts +66 -0
- package/lib/injections.js +83 -0
- package/lib/injections.js.map +1 -0
- package/lib/notice.d.ts +60 -0
- package/lib/notice.js +84 -0
- package/lib/notice.js.map +1 -0
- package/lib/provider.d.ts +44 -0
- package/lib/provider.js +66 -0
- package/lib/provider.js.map +1 -0
- package/lib/redact.d.ts +24 -0
- package/lib/redact.js +85 -0
- package/lib/redact.js.map +1 -0
- package/lib/registry.d.ts +120 -0
- package/lib/registry.js +130 -0
- package/lib/registry.js.map +1 -0
- package/lib/render.d.ts +40 -0
- package/lib/render.js +280 -0
- package/lib/render.js.map +1 -0
- package/lib/repo.d.ts +78 -0
- package/lib/repo.js +184 -0
- package/lib/repo.js.map +1 -0
- package/lib/rules-commands.d.ts +24 -0
- package/lib/rules-commands.js +138 -0
- package/lib/rules-commands.js.map +1 -0
- package/lib/rules.d.ts +90 -0
- package/lib/rules.js +185 -0
- package/lib/rules.js.map +1 -0
- package/lib/search.d.ts +48 -0
- package/lib/search.js +152 -0
- package/lib/search.js.map +1 -0
- package/lib/signature.d.ts +28 -0
- package/lib/signature.js +149 -0
- package/lib/signature.js.map +1 -0
- package/lib/store.d.ts +130 -0
- package/lib/store.js +235 -0
- package/lib/store.js.map +1 -0
- package/lib/sync.d.ts +183 -0
- package/lib/sync.js +719 -0
- package/lib/sync.js.map +1 -0
- package/lib/tools.d.ts +84 -0
- package/lib/tools.js +581 -0
- package/lib/tools.js.map +1 -0
- package/lib/treedx/client.d.ts +57 -0
- package/lib/treedx/client.js +82 -0
- package/lib/treedx/client.js.map +1 -0
- package/lib/treedx/provider.d.ts +39 -0
- package/lib/treedx/provider.js +479 -0
- package/lib/treedx/provider.js.map +1 -0
- package/lib/treedx/state.d.ts +27 -0
- package/lib/treedx/state.js +65 -0
- package/lib/treedx/state.js.map +1 -0
- package/lib/types.d.ts +100 -0
- package/lib/types.js +19 -0
- package/lib/types.js.map +1 -0
- package/lib/vocabulary.d.ts +41 -0
- package/lib/vocabulary.js +118 -0
- package/lib/vocabulary.js.map +1 -0
- package/package.json +132 -0
- package/presets/chapters/agent.cordis.yml +272 -0
- package/presets/chapters/preset.yml +3 -0
- package/presets/probe-roots/agent.cordis.yml +11 -0
- package/presets/probe-roots/preset.yml +3 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* P3 S2 — the /chapters-rule command family (record §7.2), as a pure-ish
|
|
3
|
+
* module so the lifecycle is testable without a commands service.
|
|
4
|
+
*
|
|
5
|
+
* /chapters-rule add <category> <text…>
|
|
6
|
+
* /chapters-rule list [--all | --proposed | --category <c>]
|
|
7
|
+
* /chapters-rule approve <id> | revoke <id>
|
|
8
|
+
*
|
|
9
|
+
* Identity: '<harnessId>/<NNN>'. Files are write-once; status is a per-machine
|
|
10
|
+
* curation fact (§15 amendment) appended to the mirror's own edits/<harness>/
|
|
11
|
+
* curation.jsonl and pushed immediately (offline-safe: the local commit still
|
|
12
|
+
* happens; push coalesces later). Commands never enter the model context.
|
|
13
|
+
*/
|
|
14
|
+
import fs from 'node:fs';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import { renderRuleFile, rulePath, collectMirrorRules, appendRuleStatusFact, } from "./rules.js";
|
|
17
|
+
const HELP = 'usage: /chapters-rule add <category> <text> | list [--all|--proposed|--category <c>] | approve <id> | revoke <id>';
|
|
18
|
+
export async function rulesCommand(io, args) {
|
|
19
|
+
const parts = args.trim().split(/\s+/).filter((p) => p.length > 0);
|
|
20
|
+
const verb = parts[0] ?? 'help';
|
|
21
|
+
const cwd = io.cwd();
|
|
22
|
+
const project = io.projectFor(cwd);
|
|
23
|
+
if (project === undefined) {
|
|
24
|
+
return { kind: 'error', text: 'no knowledge repository is linked for this workspace — link one with /chapters-link first (rules are shared state)' };
|
|
25
|
+
}
|
|
26
|
+
const mirror = io.mirrorDir(cwd);
|
|
27
|
+
// every rule known to THIS machine: mirror files (all authors) ∪ own domain records (pre-sync)
|
|
28
|
+
const known = () => {
|
|
29
|
+
const byId = new Map();
|
|
30
|
+
for (const r of collectMirrorRules(mirror, project.projectKey, io.harnessId)) {
|
|
31
|
+
byId.set(r.id, { id: r.id, category: r.category, title: r.title, status: r.status, sourceSession: '' });
|
|
32
|
+
}
|
|
33
|
+
for (const [, rec] of io.store.rules()) {
|
|
34
|
+
if (rec.projectKey !== project.projectKey)
|
|
35
|
+
continue;
|
|
36
|
+
const prev = byId.get(rec.id);
|
|
37
|
+
byId.set(rec.id, { id: rec.id, category: rec.category, title: rec.title, status: prev?.status ?? 'proposed', sourceSession: io.sourceSession ?? 'manual' });
|
|
38
|
+
}
|
|
39
|
+
return [...byId.values()].sort((a, b) => a.id < b.id ? -1 : 1);
|
|
40
|
+
};
|
|
41
|
+
if (verb === 'add') {
|
|
42
|
+
const category = parts[1];
|
|
43
|
+
const body = parts.slice(2).join(' ');
|
|
44
|
+
if (category === undefined || body.length < 10) {
|
|
45
|
+
return { kind: 'error', text: `add needs a category and at least a sentence of rule text. ${HELP}` };
|
|
46
|
+
}
|
|
47
|
+
let number = 1;
|
|
48
|
+
for (const [id] of io.store.rules()) {
|
|
49
|
+
const m = new RegExp(`^${io.harnessId}/(\\d{3})$`).exec(id);
|
|
50
|
+
if (m !== null)
|
|
51
|
+
number = Math.max(number, Number(m[1]) + 1);
|
|
52
|
+
}
|
|
53
|
+
const title = body.split(/[.!?\n]/)[0].trim().slice(0, 60);
|
|
54
|
+
let rel = rulePath(io.storeRoot, io.harnessId, number, category, title);
|
|
55
|
+
let abs = path.join(cwd, rel);
|
|
56
|
+
// The domain may be fresh while disk is not (e2e rebuilds the home every
|
|
57
|
+
// boot; per-machine rule files are write-once): walk past any number the
|
|
58
|
+
// disk already holds rather than refuse. Refusing was the r39-era e2e
|
|
59
|
+
// deadlock — add could never land again in a dirty workspace. The walk is
|
|
60
|
+
// keyed on the NUMBER PREFIX, not the exact path: a disk ghost of a dead
|
|
61
|
+
// rule (its title killed, its domain record gone) still owns its number —
|
|
62
|
+
// reusing that number for a different title would mint two files sharing
|
|
63
|
+
// one id (approve-by-id would be ambiguous).
|
|
64
|
+
const rulesDir = path.join(cwd, io.storeRoot, 'rules', io.harnessId);
|
|
65
|
+
const numberTaken = (n) => {
|
|
66
|
+
const prefix = `${String(n).padStart(3, '0')}-`;
|
|
67
|
+
try {
|
|
68
|
+
return fs.readdirSync(rulesDir).some((f) => f.startsWith(prefix));
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
for (let hops = 0; (numberTaken(number) || fs.existsSync(abs)) && hops < 200; hops += 1) {
|
|
75
|
+
number += 1;
|
|
76
|
+
rel = rulePath(io.storeRoot, io.harnessId, number, category, title);
|
|
77
|
+
abs = path.join(cwd, rel);
|
|
78
|
+
}
|
|
79
|
+
if (numberTaken(number) || fs.existsSync(abs))
|
|
80
|
+
return { kind: 'error', text: `refusing to overwrite ${rel} (rule files are write-once; 200 number slots exhausted?)` };
|
|
81
|
+
const id = `${io.harnessId}/${String(number).padStart(3, '0')}`;
|
|
82
|
+
const rec = {
|
|
83
|
+
id, projectKey: project.projectKey, number, path: rel,
|
|
84
|
+
category: category.toLowerCase(), title, sourceSession: io.sourceSession ?? 'manual', at: io.now().toISOString(),
|
|
85
|
+
};
|
|
86
|
+
try {
|
|
87
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
88
|
+
fs.writeFileSync(abs, renderRuleFile({ number, category: rec.category, title, sourceSession: io.sourceSession ?? 'manual', at: rec.at, body: body + '\n' }));
|
|
89
|
+
await io.store.putRule(id, rec);
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
return { kind: 'error', text: `rule add failed: ${String(error?.message ?? error).slice(0, 160)}` };
|
|
93
|
+
}
|
|
94
|
+
await io.syncNow(cwd);
|
|
95
|
+
return { kind: 'success', text: `rule ${id} proposed (category ${rec.category}) — approve it into this machine's core set with: /chapters-rule approve ${id}` };
|
|
96
|
+
}
|
|
97
|
+
if (verb === 'list') {
|
|
98
|
+
const all = parts.includes('--all');
|
|
99
|
+
const proposedOnly = parts.includes('--proposed');
|
|
100
|
+
const catIdx = parts.indexOf('--category');
|
|
101
|
+
const cat = catIdx > -1 ? (parts[catIdx + 1] ?? '').toLowerCase() : '';
|
|
102
|
+
let rows = known();
|
|
103
|
+
if (cat !== '')
|
|
104
|
+
rows = rows.filter((r) => r.category === cat);
|
|
105
|
+
if (!all && !proposedOnly)
|
|
106
|
+
rows = rows.filter((r) => r.status !== 'revoked');
|
|
107
|
+
if (proposedOnly)
|
|
108
|
+
rows = rows.filter((r) => r.status === 'proposed');
|
|
109
|
+
if (rows.length === 0)
|
|
110
|
+
return { kind: 'success', text: 'no rules match' };
|
|
111
|
+
const text = rows.map((r) => `${r.id.padEnd(12)} ${r.status.padEnd(9)} ${r.category.padEnd(14)} ${r.title}`).join('\n');
|
|
112
|
+
return { kind: 'success', text: `id status category title\n${text}` };
|
|
113
|
+
}
|
|
114
|
+
if (verb === 'approve' || verb === 'revoke') {
|
|
115
|
+
let id = parts[1] ?? '';
|
|
116
|
+
if (id === '')
|
|
117
|
+
return { kind: 'error', text: `${verb} needs a rule id (see /chapters-rule list)` };
|
|
118
|
+
if (!id.includes('/'))
|
|
119
|
+
id = `${io.harnessId}/${id.padStart(3, '0')}`;
|
|
120
|
+
const exists = known().some((r) => r.id === id);
|
|
121
|
+
if (!exists)
|
|
122
|
+
return { kind: 'error', text: `no rule ${id} in project ${project.slug} — unknown id (list shows what exists)` };
|
|
123
|
+
try {
|
|
124
|
+
// 'approve' is the verb; 'core' is the status — conflating them writes a
|
|
125
|
+
// fact no resolver recognizes (caught by unit test)
|
|
126
|
+
appendRuleStatusFact(mirror, io.harnessId, { rule: id, status: verb === 'approve' ? 'core' : 'revoked', at: io.now().toISOString() });
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
return { kind: 'error', text: `${verb} failed: ${String(error?.message ?? error).slice(0, 160)}` };
|
|
130
|
+
}
|
|
131
|
+
await io.syncNow(cwd);
|
|
132
|
+
return { kind: 'success', text: verb === 'approve'
|
|
133
|
+
? `rule ${id} is CORE on THIS machine (other machines approve independently) — it will render into continuations from this workspace`
|
|
134
|
+
: `rule ${id} revoked on this machine (out of the notice, still in the repo)` };
|
|
135
|
+
}
|
|
136
|
+
return { kind: 'success', text: HELP };
|
|
137
|
+
}
|
|
138
|
+
//# sourceMappingURL=rules-commands.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rules-commands.js","sourceRoot":"","sources":["../src/rules-commands.ts"],"names":[],"mappings":"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"}
|
package/lib/rules.d.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const RULE_STATUS: readonly ["proposed", "core", "revoked"];
|
|
3
|
+
export type RuleStatus = (typeof RULE_STATUS)[number];
|
|
4
|
+
export interface RuleRecord {
|
|
5
|
+
id: string;
|
|
6
|
+
projectKey: string;
|
|
7
|
+
number: number;
|
|
8
|
+
path: string;
|
|
9
|
+
category: string;
|
|
10
|
+
title: string;
|
|
11
|
+
sourceSession: string;
|
|
12
|
+
at: string;
|
|
13
|
+
}
|
|
14
|
+
export declare const ruleRecordSchema: z.ZodObject<{
|
|
15
|
+
id: z.ZodString;
|
|
16
|
+
projectKey: z.ZodString;
|
|
17
|
+
number: z.ZodNumber;
|
|
18
|
+
path: z.ZodString;
|
|
19
|
+
category: z.ZodString;
|
|
20
|
+
title: z.ZodString;
|
|
21
|
+
sourceSession: z.ZodString;
|
|
22
|
+
at: z.ZodString;
|
|
23
|
+
}, z.core.$strip>;
|
|
24
|
+
export declare const ruleSlug: (category: string, title: string) => string;
|
|
25
|
+
export declare const rulePath: (storeRoot: string, harnessId: string, number: number, category: string, title: string) => string;
|
|
26
|
+
/** one write, ever: rules are proposed by their author and never rewritten (status rides facts) */
|
|
27
|
+
export declare function renderRuleFile(input: {
|
|
28
|
+
number: number;
|
|
29
|
+
category: string;
|
|
30
|
+
title: string;
|
|
31
|
+
sourceSession: string;
|
|
32
|
+
at: string;
|
|
33
|
+
body: string;
|
|
34
|
+
}): string;
|
|
35
|
+
export interface ParsedRule {
|
|
36
|
+
number: number;
|
|
37
|
+
category: string;
|
|
38
|
+
title: string;
|
|
39
|
+
fileStatus: RuleStatus;
|
|
40
|
+
sourceSession: string;
|
|
41
|
+
body: string;
|
|
42
|
+
}
|
|
43
|
+
export declare function parseRuleFile(text: string): ParsedRule | null;
|
|
44
|
+
export interface RuleStatusFact {
|
|
45
|
+
rule: string;
|
|
46
|
+
status: RuleStatus;
|
|
47
|
+
at: string;
|
|
48
|
+
}
|
|
49
|
+
/** the machine's own facts, last-write-wins per rule (append-only file order) */
|
|
50
|
+
export declare function loadRuleStatusFacts(mirrorDir: string, harnessId: string): Map<string, RuleStatusFact>;
|
|
51
|
+
/** append-only, like every other curation write */
|
|
52
|
+
export declare function appendRuleStatusFact(mirrorDir: string, harnessId: string, fact: RuleStatusFact): void;
|
|
53
|
+
export declare function effectiveStatus(ruleId: string, facts: Map<string, RuleStatusFact>, fileStatus: RuleStatus): RuleStatus;
|
|
54
|
+
export interface MirrorRule {
|
|
55
|
+
id: string;
|
|
56
|
+
relPath: string;
|
|
57
|
+
status: RuleStatus;
|
|
58
|
+
category: string;
|
|
59
|
+
title: string;
|
|
60
|
+
body: string;
|
|
61
|
+
bodyTokens: number;
|
|
62
|
+
}
|
|
63
|
+
/** every rule file the mirror knows for one project, with THIS machine's effective status */
|
|
64
|
+
export declare function collectMirrorRules(mirrorDir: string, projectKey: string, harnessId: string): MirrorRule[];
|
|
65
|
+
export type RulesSectionResult = {
|
|
66
|
+
kind: 'none';
|
|
67
|
+
} | {
|
|
68
|
+
kind: 'ok';
|
|
69
|
+
text: string;
|
|
70
|
+
tokens: number;
|
|
71
|
+
} | {
|
|
72
|
+
kind: 'refusal';
|
|
73
|
+
reason: string;
|
|
74
|
+
perRule: Array<{
|
|
75
|
+
id: string;
|
|
76
|
+
tokens: number;
|
|
77
|
+
}>;
|
|
78
|
+
cap: number;
|
|
79
|
+
total: number;
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* §7.3: the notice's rules block. `kind: 'none'` (empty rules or no corpus)
|
|
83
|
+
* MUST produce zero injected bytes at the call site — the existing tape
|
|
84
|
+
* corpus depends on it. Overflow refuses with per-rule numbers, never clips.
|
|
85
|
+
*/
|
|
86
|
+
export declare function buildRulesSection(mirrorDir: string, opts: {
|
|
87
|
+
projectKey: string;
|
|
88
|
+
harnessId: string;
|
|
89
|
+
budgetTokens: number;
|
|
90
|
+
}): RulesSectionResult;
|
package/lib/rules.js
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* P3 — rules are chapters of a different kind (record §7.1): same file
|
|
3
|
+
* format, same sync, searched like anything else; this module adds ONLY the
|
|
4
|
+
* lifecycle-shaped parts:
|
|
5
|
+
*
|
|
6
|
+
* - the rule file (written ONCE, status: proposed, immutable thereafter),
|
|
7
|
+
* - the per-machine status resolution (§15 amendment 2026-09-20): effective
|
|
8
|
+
* status comes from the APPROVING MACHINE's own curation facts
|
|
9
|
+
* (edits/<harnessId>/curation.jsonl, `rule-status` entries), never from a
|
|
10
|
+
* file rewrite — authorship stays partitioned, and a rule proposed on one
|
|
11
|
+
* machine affects no other machine's notice until a human there approves,
|
|
12
|
+
* - the notice section (§7.3): core rules verbatim + a category index,
|
|
13
|
+
* budgeted, refuse-with-numbers on overflow, and — load-bearing for the
|
|
14
|
+
* tape corpus — BYTE-IDENTICAL NOTHING when there are no rules: an empty
|
|
15
|
+
* project renders no section at all.
|
|
16
|
+
*/
|
|
17
|
+
import fs from 'node:fs';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import { z } from 'zod';
|
|
20
|
+
import { estimateTokens, sha256 } from "./render.js";
|
|
21
|
+
import { parseCuration } from "./indexing.js";
|
|
22
|
+
import { parseChapterFile } from "./enrich-store.js";
|
|
23
|
+
export const RULE_STATUS = ['proposed', 'core', 'revoked'];
|
|
24
|
+
export const ruleRecordSchema = z.object({
|
|
25
|
+
id: z.string().min(3),
|
|
26
|
+
projectKey: z.string().min(1),
|
|
27
|
+
number: z.number().int().positive(),
|
|
28
|
+
path: z.string().min(1),
|
|
29
|
+
category: z.string().min(1),
|
|
30
|
+
title: z.string(),
|
|
31
|
+
sourceSession: z.string(),
|
|
32
|
+
at: z.string(),
|
|
33
|
+
});
|
|
34
|
+
export const ruleSlug = (category, title) => {
|
|
35
|
+
const s = (t) => t.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 40);
|
|
36
|
+
return `${s(category) || 'rule'}-${s(title) || 'untitled'}`;
|
|
37
|
+
};
|
|
38
|
+
export const rulePath = (storeRoot, harnessId, number, category, title) => path.posix.join(storeRoot, 'rules', harnessId, `${String(number).padStart(3, '0')}-${ruleSlug(category, title)}.md`);
|
|
39
|
+
/** one write, ever: rules are proposed by their author and never rewritten (status rides facts) */
|
|
40
|
+
export function renderRuleFile(input) {
|
|
41
|
+
return [
|
|
42
|
+
'---',
|
|
43
|
+
`number: ${input.number}`,
|
|
44
|
+
'kind: rule',
|
|
45
|
+
`category: ${JSON.stringify(input.category)}`,
|
|
46
|
+
`title: ${JSON.stringify(input.title)}`,
|
|
47
|
+
'status: proposed',
|
|
48
|
+
`sourceSession: ${JSON.stringify(input.sourceSession)}`,
|
|
49
|
+
`authoredAt: ${JSON.stringify(input.at)}`,
|
|
50
|
+
`sha256: ${sha256(input.body)}`,
|
|
51
|
+
'---',
|
|
52
|
+
input.body,
|
|
53
|
+
].join('\n');
|
|
54
|
+
}
|
|
55
|
+
export function parseRuleFile(text) {
|
|
56
|
+
try {
|
|
57
|
+
const doc = parseChapterFile(text);
|
|
58
|
+
const fm = new Map();
|
|
59
|
+
for (const line of doc.fmLines) {
|
|
60
|
+
const m = /^([A-Za-z][\w]*):\s*(.*)$/.exec(line);
|
|
61
|
+
if (m !== null)
|
|
62
|
+
fm.set(m[1], m[2].trim());
|
|
63
|
+
}
|
|
64
|
+
if (fm.get('kind') !== 'rule')
|
|
65
|
+
return null;
|
|
66
|
+
const unq = (v, d) => (v ?? d).replace(/^"|"$/g, '');
|
|
67
|
+
const rawStatus = unq(fm.get('status'), 'proposed');
|
|
68
|
+
return {
|
|
69
|
+
number: Number(fm.get('number') ?? '0'),
|
|
70
|
+
category: unq(fm.get('category'), 'general'),
|
|
71
|
+
title: unq(fm.get('title'), ''),
|
|
72
|
+
fileStatus: RULE_STATUS.includes(rawStatus) ? rawStatus : 'proposed',
|
|
73
|
+
sourceSession: unq(fm.get('sourceSession'), ''),
|
|
74
|
+
body: doc.body,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/** the machine's own facts, last-write-wins per rule (append-only file order) */
|
|
82
|
+
export function loadRuleStatusFacts(mirrorDir, harnessId) {
|
|
83
|
+
const out = new Map();
|
|
84
|
+
const file = path.join(mirrorDir, 'edits', harnessId, 'curation.jsonl');
|
|
85
|
+
if (!fs.existsSync(file))
|
|
86
|
+
return out;
|
|
87
|
+
for (const f of parseCuration(fs.readFileSync(file, 'utf8'))) {
|
|
88
|
+
if (f.type !== 'rule-status')
|
|
89
|
+
continue;
|
|
90
|
+
const f2 = f;
|
|
91
|
+
if (typeof f2.rule !== 'string' || typeof f2.status !== 'string')
|
|
92
|
+
continue;
|
|
93
|
+
if (!RULE_STATUS.includes(f2.status))
|
|
94
|
+
continue;
|
|
95
|
+
out.set(f2.rule, { rule: f2.rule, status: f2.status, at: f2.at ?? '' });
|
|
96
|
+
}
|
|
97
|
+
return out;
|
|
98
|
+
}
|
|
99
|
+
/** append-only, like every other curation write */
|
|
100
|
+
export function appendRuleStatusFact(mirrorDir, harnessId, fact) {
|
|
101
|
+
const dir = path.join(mirrorDir, 'edits', harnessId);
|
|
102
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
103
|
+
fs.appendFileSync(path.join(dir, 'curation.jsonl'), JSON.stringify({ type: 'rule-status', ...fact }) + '\n');
|
|
104
|
+
}
|
|
105
|
+
export function effectiveStatus(ruleId, facts, fileStatus) {
|
|
106
|
+
return facts.get(ruleId)?.status ?? fileStatus;
|
|
107
|
+
}
|
|
108
|
+
/** every rule file the mirror knows for one project, with THIS machine's effective status */
|
|
109
|
+
export function collectMirrorRules(mirrorDir, projectKey, harnessId) {
|
|
110
|
+
const root = path.join(mirrorDir, 'rules', projectKey);
|
|
111
|
+
if (!fs.existsSync(root))
|
|
112
|
+
return [];
|
|
113
|
+
const facts = loadRuleStatusFacts(mirrorDir, harnessId);
|
|
114
|
+
const out = [];
|
|
115
|
+
const walk = (dir, rel) => {
|
|
116
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
117
|
+
const p = path.join(dir, e.name);
|
|
118
|
+
if (e.isDirectory()) {
|
|
119
|
+
walk(p, rel === '' ? e.name : path.join(rel, e.name));
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (!e.name.endsWith('.md'))
|
|
123
|
+
continue;
|
|
124
|
+
const relPath = path.posix.join('rules', projectKey, rel === '' ? e.name : `${rel}/${e.name}`);
|
|
125
|
+
const rule = parseRuleFile(fs.readFileSync(p, 'utf8'));
|
|
126
|
+
if (rule === null)
|
|
127
|
+
continue;
|
|
128
|
+
const harness = e.name === '' ? harnessId : path.basename(path.dirname(p));
|
|
129
|
+
const id = `${harness}/${String(rule.number).padStart(3, '0')}`;
|
|
130
|
+
const status = effectiveStatus(id, facts, rule.fileStatus);
|
|
131
|
+
out.push({ id, relPath, status, category: rule.category, title: rule.title, body: rule.body.trim(), bodyTokens: estimateTokens(rule.body.trim()) });
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
walk(root, '');
|
|
135
|
+
return out.sort((a, b) => a.category < b.category ? -1 : a.category > b.category ? 1 : a.id < b.id ? -1 : 1);
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* §7.3: the notice's rules block. `kind: 'none'` (empty rules or no corpus)
|
|
139
|
+
* MUST produce zero injected bytes at the call site — the existing tape
|
|
140
|
+
* corpus depends on it. Overflow refuses with per-rule numbers, never clips.
|
|
141
|
+
*/
|
|
142
|
+
export function buildRulesSection(mirrorDir, opts) {
|
|
143
|
+
const rules = collectMirrorRules(mirrorDir, opts.projectKey, opts.harnessId);
|
|
144
|
+
const core = rules.filter((r) => r.status === 'core');
|
|
145
|
+
const categories = new Map();
|
|
146
|
+
for (const r of rules) {
|
|
147
|
+
if (r.status === 'revoked')
|
|
148
|
+
continue;
|
|
149
|
+
const prev = categories.get(r.category);
|
|
150
|
+
categories.set(r.category, {
|
|
151
|
+
count: (prev?.count ?? 0) + 1,
|
|
152
|
+
essence: prev?.essence ?? r.title,
|
|
153
|
+
samplePath: prev?.samplePath ?? r.relPath,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
if (core.length === 0 && categories.size === 0)
|
|
157
|
+
return { kind: 'none' };
|
|
158
|
+
const lines = [];
|
|
159
|
+
if (core.length > 0) {
|
|
160
|
+
lines.push('## CORE RULES (approved for this project — follow them)');
|
|
161
|
+
let total = 0;
|
|
162
|
+
for (const r of core)
|
|
163
|
+
total += r.bodyTokens;
|
|
164
|
+
if (total > opts.budgetTokens) {
|
|
165
|
+
return {
|
|
166
|
+
kind: 'refusal',
|
|
167
|
+
reason: `core rules exceed their budget: ${total} tokens over a cap of ${opts.budgetTokens}. Revoke one ("/chapters-rule revoke <id>") or split a category — the notice never clips rules.`,
|
|
168
|
+
perRule: core.map((r) => ({ id: r.id, tokens: r.bodyTokens })),
|
|
169
|
+
cap: opts.budgetTokens,
|
|
170
|
+
total,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
for (const r of core)
|
|
174
|
+
lines.push(`[${r.id}] (${r.category}) ${r.body}`, '');
|
|
175
|
+
}
|
|
176
|
+
if (categories.size > 0) {
|
|
177
|
+
lines.push('Rule categories (load a category from the repo when this session\u2019s topics overlap it):');
|
|
178
|
+
for (const [cat, m] of [...categories.entries()].sort((a, b) => a[0] < b[0] ? -1 : 1)) {
|
|
179
|
+
lines.push(`- ${cat} (${m.count}): ${m.essence} — e.g. ${m.samplePath}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
const text = lines.join('\n').trimEnd();
|
|
183
|
+
return { kind: 'ok', text, tokens: estimateTokens(text) };
|
|
184
|
+
}
|
|
185
|
+
//# sourceMappingURL=rules.js.map
|
package/lib/rules.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rules.js","sourceRoot":"","sources":["../src/rules.ts"],"names":[],"mappings":"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"}
|
package/lib/search.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { type IndexEntry } from './indexing.ts';
|
|
2
|
+
export interface SearchResult {
|
|
3
|
+
score: number;
|
|
4
|
+
date: string;
|
|
5
|
+
kind: 'chapter' | 'rule';
|
|
6
|
+
title: string;
|
|
7
|
+
path: string;
|
|
8
|
+
/** S5 stitch view: every member file behind a coherented entry. */
|
|
9
|
+
paths?: string[];
|
|
10
|
+
topics: string[];
|
|
11
|
+
}
|
|
12
|
+
export interface SearchOutcome {
|
|
13
|
+
results: SearchResult[];
|
|
14
|
+
/** Every match found (results may be a packed prefix of this). */
|
|
15
|
+
total: number;
|
|
16
|
+
shown: number;
|
|
17
|
+
budget: {
|
|
18
|
+
requested: number;
|
|
19
|
+
used: number;
|
|
20
|
+
remaining: number;
|
|
21
|
+
};
|
|
22
|
+
line: string;
|
|
23
|
+
}
|
|
24
|
+
/** Term-hit score: topic hits 2x, title 1x, summary 1x. Zero = no match, full stop. */
|
|
25
|
+
export declare function baseScore(entry: IndexEntry, terms: readonly string[], summary: string): number;
|
|
26
|
+
/**
|
|
27
|
+
* The full score: term hits plus a recency tie-break. Recency ONLY breaks
|
|
28
|
+
* ties between real matches — a fresh mtime must never rank an entry that
|
|
29
|
+
* matches no term (that would surface unrelated fresh files on any query).
|
|
30
|
+
*/
|
|
31
|
+
export declare function scoreEntry(entry: IndexEntry, terms: readonly string[], now: number, summary: string): number;
|
|
32
|
+
/** Load the corpus from a clone directory: chapters + rules, frontmatter-parsed. */
|
|
33
|
+
export declare function loadCorpus(cloneDir: string): {
|
|
34
|
+
entries: IndexEntry[];
|
|
35
|
+
summaries: Map<string, string>;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* The search itself: score, sort (score desc, then path for determinism),
|
|
39
|
+
* format lines `score | date | kind | title | path (topics: …)`, and pack
|
|
40
|
+
* lines to the requested token budget. The agent sees `total` vs `shown` so
|
|
41
|
+
* it can re-request with a larger budget — no silent truncation.
|
|
42
|
+
*/
|
|
43
|
+
export declare function searchKnowledge(cloneDir: string, query: string, maxTokens: number, opts?: {
|
|
44
|
+
now?: number;
|
|
45
|
+
projectKey?: string;
|
|
46
|
+
harnessId?: string;
|
|
47
|
+
rulesCoreBonus?: number;
|
|
48
|
+
}): SearchOutcome;
|
package/lib/search.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Knowledge search (knowledge-repo.md §8): a DETERMINISTIC scorer over the
|
|
3
|
+
* mirrored corpus (the clone is the corpus — it holds local + pulled files),
|
|
4
|
+
* packed to a token budget the AGENT chooses (it knows its window; we
|
|
5
|
+
* measure with the project's one estimator). No model in the loop, ever —
|
|
6
|
+
* reproducible, inspectable, and free.
|
|
7
|
+
*/
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { entryFromChapter } from "./indexing.js";
|
|
11
|
+
function entryBody(text) {
|
|
12
|
+
const i = text.indexOf('\n---\n');
|
|
13
|
+
return i === -1 ? '' : text.slice(i + 5).trim();
|
|
14
|
+
}
|
|
15
|
+
import { estimateTokens } from "./render.js";
|
|
16
|
+
import { collectMirrorRules } from "./rules.js";
|
|
17
|
+
import { stitchFragments } from "./indexing.js";
|
|
18
|
+
const DAY = 24 * 60 * 60 * 1000;
|
|
19
|
+
const tokenize = (text) => text.toLowerCase().split(/[^a-z0-9/._-]+/).filter((t) => t.length >= 2 && t !== 'the' && t !== 'and');
|
|
20
|
+
const recencyBonus = (mtime, now) => {
|
|
21
|
+
if (mtime === undefined)
|
|
22
|
+
return 0;
|
|
23
|
+
const ageDays = (now - mtime) / DAY;
|
|
24
|
+
if (ageDays <= 30)
|
|
25
|
+
return 1;
|
|
26
|
+
if (ageDays <= 180)
|
|
27
|
+
return 0.5;
|
|
28
|
+
return 0;
|
|
29
|
+
};
|
|
30
|
+
/** Term-hit score: topic hits 2x, title 1x, summary 1x. Zero = no match, full stop. */
|
|
31
|
+
export function baseScore(entry, terms, summary) {
|
|
32
|
+
let score = 0;
|
|
33
|
+
for (const t of terms) {
|
|
34
|
+
if (entry.topics.some((topic) => topic.toLowerCase().includes(t)))
|
|
35
|
+
score += 2;
|
|
36
|
+
if (entry.category !== undefined && entry.category.toLowerCase().includes(t))
|
|
37
|
+
score += 1.5;
|
|
38
|
+
if (entry.title.toLowerCase().includes(t))
|
|
39
|
+
score += 1;
|
|
40
|
+
if (summary.toLowerCase().includes(t))
|
|
41
|
+
score += 1;
|
|
42
|
+
}
|
|
43
|
+
return score;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The full score: term hits plus a recency tie-break. Recency ONLY breaks
|
|
47
|
+
* ties between real matches — a fresh mtime must never rank an entry that
|
|
48
|
+
* matches no term (that would surface unrelated fresh files on any query).
|
|
49
|
+
*/
|
|
50
|
+
export function scoreEntry(entry, terms, now, summary) {
|
|
51
|
+
const base = baseScore(entry, terms, summary);
|
|
52
|
+
return base === 0 ? 0 : base + recencyBonus(entry.mtime, now);
|
|
53
|
+
}
|
|
54
|
+
/** Load the corpus from a clone directory: chapters + rules, frontmatter-parsed. */
|
|
55
|
+
export function loadCorpus(cloneDir) {
|
|
56
|
+
const entries = [];
|
|
57
|
+
const summaries = new Map();
|
|
58
|
+
// rel paths mirror the REPO layout (chapters/<key>/…, rules/<key>/…) so
|
|
59
|
+
// search results are directly readable paths and project scoping is a
|
|
60
|
+
// prefix match on the second path segment.
|
|
61
|
+
const collect = (top, kind) => {
|
|
62
|
+
const root = path.join(cloneDir, top);
|
|
63
|
+
if (!fs.existsSync(root))
|
|
64
|
+
return;
|
|
65
|
+
const walk = (dir, rel) => {
|
|
66
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
67
|
+
const p = path.join(dir, e.name);
|
|
68
|
+
if (e.isDirectory()) {
|
|
69
|
+
walk(p, rel === '' ? e.name : path.join(rel, e.name));
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (e.name.endsWith('.md')) {
|
|
73
|
+
const relPath = rel === '' ? e.name : path.join(rel, e.name);
|
|
74
|
+
const text = fs.readFileSync(p, 'utf8');
|
|
75
|
+
const entry = entryFromChapter(path.join(top, relPath), text, fs.statSync(p).mtimeMs);
|
|
76
|
+
entries.push({ ...entry, kind });
|
|
77
|
+
const fm = /^---[\s\S]*?summary:\s*(.*)$/m.exec(text);
|
|
78
|
+
if (fm !== null)
|
|
79
|
+
summaries.set(entry.path, fm[1].trim().replace(/^"|"$/g, ''));
|
|
80
|
+
// P3: rules carry no summary field — their BODY is the scannable
|
|
81
|
+
// surface; score terms against it like a chapter's summary
|
|
82
|
+
else if (entry.kind === 'rule')
|
|
83
|
+
summaries.set(entry.path, entryBody(text).slice(0, 500));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
walk(root, '');
|
|
88
|
+
};
|
|
89
|
+
collect('chapters', 'chapter');
|
|
90
|
+
collect('rules', 'rule');
|
|
91
|
+
return { entries: stitchFragments(entries), summaries }; // S5: search sees stitched views too
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* The search itself: score, sort (score desc, then path for determinism),
|
|
95
|
+
* format lines `score | date | kind | title | path (topics: …)`, and pack
|
|
96
|
+
* lines to the requested token budget. The agent sees `total` vs `shown` so
|
|
97
|
+
* it can re-request with a larger budget — no silent truncation.
|
|
98
|
+
*/
|
|
99
|
+
export function searchKnowledge(cloneDir, query, maxTokens, opts = {}) {
|
|
100
|
+
const now = opts.now ?? Date.now();
|
|
101
|
+
const terms = tokenize(query);
|
|
102
|
+
// P3 §8: rules that THIS machine approved get a small, explicit bonus —
|
|
103
|
+
// computed here (per-machine), never baked into the shared index shards.
|
|
104
|
+
let coreRuleIds = null;
|
|
105
|
+
if (opts.harnessId !== undefined && opts.rulesCoreBonus !== undefined && opts.rulesCoreBonus > 0) {
|
|
106
|
+
try {
|
|
107
|
+
coreRuleIds = new Set(collectMirrorRules(cloneDir, opts.projectKey ?? '', opts.harnessId)
|
|
108
|
+
.filter((r) => r.status === 'core').map((r) => r.relPath));
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
coreRuleIds = null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (terms.length === 0) {
|
|
115
|
+
return { results: [], total: 0, shown: 0, budget: { requested: maxTokens, used: 0, remaining: maxTokens }, line: '' };
|
|
116
|
+
}
|
|
117
|
+
const { entries, summaries } = loadCorpus(cloneDir);
|
|
118
|
+
const scoped = opts.projectKey !== undefined
|
|
119
|
+
? entries.filter((e) => e.path.startsWith(`chapters/${opts.projectKey}`) || e.path.startsWith(`rules/${opts.projectKey}`))
|
|
120
|
+
: entries;
|
|
121
|
+
const scored = scoped
|
|
122
|
+
.map((e) => ({ e, score: scoreEntry(e, terms, now, summaries.get(e.path) ?? '') + (coreRuleIds !== null && e.kind === 'rule' && coreRuleIds.has(e.path) ? (opts.rulesCoreBonus ?? 0) : 0) }))
|
|
123
|
+
.filter((s) => s.score > 0)
|
|
124
|
+
.sort((a, b) => b.score - a.score || (a.e.path < b.e.path ? -1 : 1));
|
|
125
|
+
const results = scored.map((s) => ({
|
|
126
|
+
score: Number(s.score.toFixed(2)),
|
|
127
|
+
date: s.e.mtime !== undefined ? new Date(s.e.mtime).toISOString().slice(0, 10) : '----',
|
|
128
|
+
kind: s.e.kind,
|
|
129
|
+
title: s.e.title,
|
|
130
|
+
path: s.e.path,
|
|
131
|
+
...(s.e.paths !== undefined ? { paths: s.e.paths } : {}),
|
|
132
|
+
topics: s.e.topics,
|
|
133
|
+
}));
|
|
134
|
+
const lines = [];
|
|
135
|
+
let used = 0;
|
|
136
|
+
for (const r of results) {
|
|
137
|
+
const line = `${r.score.toFixed(2)} | ${r.date} | ${r.kind} | ${r.title} | ${r.path}${r.paths !== undefined && r.paths.length > 1 ? ` [+${r.paths.length - 1} stitched part(s)]` : ''}${r.topics.length > 0 ? ` (topics: ${r.topics.join(', ')})` : ''}`;
|
|
138
|
+
const cost = estimateTokens(line);
|
|
139
|
+
if (lines.length > 0 && used + cost > maxTokens)
|
|
140
|
+
break;
|
|
141
|
+
lines.push(line);
|
|
142
|
+
used += cost;
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
results: results.slice(0, lines.length),
|
|
146
|
+
total: results.length,
|
|
147
|
+
shown: lines.length,
|
|
148
|
+
budget: { requested: maxTokens, used, remaining: Math.max(0, maxTokens - used) },
|
|
149
|
+
line: lines.join('\n'),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
//# sourceMappingURL=search.js.map
|