@indigoai-us/hq-cli 5.193.1 → 5.194.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.
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Read, validate, create and patch ontology entity files.
3
+ *
4
+ * The cloud read model validates every synced entity file (hq-pro
5
+ * `validator.ts`): required frontmatter fields, and `id` equal to
6
+ * sha256(type:canonical_name) unless `identity_version >= 2`. Every file this
7
+ * module produces is re-parsed and validated before it is written.
8
+ *
9
+ * Existing files are patched in place. Only three frontmatter keys are
10
+ * rewritten (`aliases`, `signal_count`, `last_updated`); every other line is
11
+ * kept byte for byte, and the result is checked to parse to the same values
12
+ * for every other key.
13
+ */
14
+ export interface ParsedEntityFile {
15
+ frontmatter: Record<string, unknown>;
16
+ body: string;
17
+ }
18
+ export interface EntityFields {
19
+ id: string;
20
+ type: string;
21
+ canonical_name: string;
22
+ aliases: string[];
23
+ signal_count: number;
24
+ identity_version?: number;
25
+ }
26
+ /**
27
+ * Parse with the YAML 1.2 core schema, as the cloud's `yaml` package does.
28
+ * js-yaml's default schema would turn unquoted timestamps into Date objects.
29
+ */
30
+ export declare function parseEntityFile(content: string): ParsedEntityFile | null;
31
+ /**
32
+ * The hq-pro validator rules this writer can break. Returns the entity's
33
+ * identity fields, or a reason string when the file is invalid.
34
+ */
35
+ export declare function validateEntityFrontmatter(fm: Record<string, unknown>): EntityFields | string;
36
+ export interface NewEntity {
37
+ type: string;
38
+ canonicalName: string;
39
+ aliases: string[];
40
+ confidence: number;
41
+ now: string;
42
+ signalCount: number;
43
+ }
44
+ /** A new entity file in the cloud schema, with a `# Name` heading. */
45
+ export declare function renderNewEntity(entity: NewEntity): string;
46
+ export interface EntityPatch {
47
+ addAliases?: string[];
48
+ signalCount?: number;
49
+ lastUpdated: string;
50
+ /** The caller changed the body, so `last_updated` moves even with no key change. */
51
+ bodyChanged?: boolean;
52
+ }
53
+ export type PatchResult = {
54
+ ok: true;
55
+ content: string;
56
+ changed: boolean;
57
+ } | {
58
+ ok: false;
59
+ reason: string;
60
+ };
61
+ /**
62
+ * Patch an existing, valid entity file. Aliases are only appended, and
63
+ * `signal_count` never goes down. When nothing but `last_updated` would
64
+ * change, the file is returned unchanged so a rerun writes nothing.
65
+ */
66
+ export declare function patchEntityFile(content: string, patch: EntityPatch): PatchResult;
67
+ export interface SignalBackRef {
68
+ signalType: string;
69
+ preview: string;
70
+ href: string;
71
+ }
72
+ /** hq-pro `previewSignalContent`: first 80 characters, one line. */
73
+ export declare function previewSignalContent(canonicalContent: string): string;
74
+ /**
75
+ * hq-pro `appendBackRefsToBody`: add `- Referenced in [<type>: <preview>](<href>)`
76
+ * lines to `## Relationships`, keyed on the link target. Existing lines are
77
+ * never removed or reworded.
78
+ */
79
+ export declare function appendBackRefs(fullContent: string, refs: SignalBackRef[]): {
80
+ content: string;
81
+ added: number;
82
+ };
83
+ //# sourceMappingURL=entity-file.d.ts.map
@@ -0,0 +1,264 @@
1
+ /**
2
+ * Read, validate, create and patch ontology entity files.
3
+ *
4
+ * The cloud read model validates every synced entity file (hq-pro
5
+ * `validator.ts`): required frontmatter fields, and `id` equal to
6
+ * sha256(type:canonical_name) unless `identity_version >= 2`. Every file this
7
+ * module produces is re-parsed and validated before it is written.
8
+ *
9
+ * Existing files are patched in place. Only three frontmatter keys are
10
+ * rewritten (`aliases`, `signal_count`, `last_updated`); every other line is
11
+ * kept byte for byte, and the result is checked to parse to the same values
12
+ * for every other key.
13
+ */
14
+ import { isDeepStrictEqual } from "node:util";
15
+ import yaml from "js-yaml";
16
+ import { entityId } from "./identity.js";
17
+ // Same delimiters as hq-pro `parser.ts`.
18
+ const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
19
+ /**
20
+ * Parse with the YAML 1.2 core schema, as the cloud's `yaml` package does.
21
+ * js-yaml's default schema would turn unquoted timestamps into Date objects.
22
+ */
23
+ export function parseEntityFile(content) {
24
+ const match = FRONTMATTER_RE.exec(content);
25
+ if (!match)
26
+ return null;
27
+ let raw;
28
+ try {
29
+ raw = yaml.load(match[1], { schema: yaml.CORE_SCHEMA });
30
+ }
31
+ catch {
32
+ return null;
33
+ }
34
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
35
+ return null;
36
+ return { frontmatter: raw, body: match[2] };
37
+ }
38
+ function isStringArray(value) {
39
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
40
+ }
41
+ /**
42
+ * The hq-pro validator rules this writer can break. Returns the entity's
43
+ * identity fields, or a reason string when the file is invalid.
44
+ */
45
+ export function validateEntityFrontmatter(fm) {
46
+ if (typeof fm.id !== "string" || fm.id === "")
47
+ return "id";
48
+ if (typeof fm.type !== "string" || fm.type === "")
49
+ return "type";
50
+ if (typeof fm.canonical_name !== "string" || fm.canonical_name === "")
51
+ return "canonical_name";
52
+ if (!isStringArray(fm.aliases))
53
+ return "aliases";
54
+ if (typeof fm.confidence !== "number" || fm.confidence < 0 || fm.confidence > 1)
55
+ return "confidence";
56
+ if (fm.status !== "draft" && fm.status !== "confirmed")
57
+ return "status";
58
+ if (!isStringArray(fm.domain))
59
+ return "domain";
60
+ if (typeof fm.first_seen !== "string" || fm.first_seen === "")
61
+ return "first_seen";
62
+ if (typeof fm.last_updated !== "string" || fm.last_updated === "")
63
+ return "last_updated";
64
+ if (typeof fm.signal_count !== "number" || !Number.isInteger(fm.signal_count) || fm.signal_count < 0) {
65
+ return "signal_count";
66
+ }
67
+ let identityVersion;
68
+ if (fm.identity_version !== undefined) {
69
+ if (typeof fm.identity_version !== "number" || !Number.isInteger(fm.identity_version) || fm.identity_version < 1) {
70
+ return "identity_version";
71
+ }
72
+ identityVersion = fm.identity_version;
73
+ }
74
+ if ((identityVersion ?? 1) < 2 && fm.id !== entityId(fm.type, fm.canonical_name))
75
+ return "id_mismatch";
76
+ return {
77
+ id: fm.id,
78
+ type: fm.type,
79
+ canonical_name: fm.canonical_name,
80
+ aliases: fm.aliases,
81
+ signal_count: fm.signal_count,
82
+ ...(identityVersion === undefined ? {} : { identity_version: identityVersion }),
83
+ };
84
+ }
85
+ /** A YAML double-quoted scalar. JSON string syntax is valid YAML. */
86
+ function quote(value) {
87
+ return JSON.stringify(value);
88
+ }
89
+ function renderAliases(aliases) {
90
+ if (aliases.length === 0)
91
+ return "aliases: []";
92
+ return ["aliases:", ...aliases.map((alias) => ` - ${quote(alias)}`)].join("\n");
93
+ }
94
+ /** A new entity file in the cloud schema, with a `# Name` heading. */
95
+ export function renderNewEntity(entity) {
96
+ return [
97
+ "---",
98
+ `id: ${entityId(entity.type, entity.canonicalName)}`,
99
+ `type: ${entity.type}`,
100
+ `canonical_name: ${quote(entity.canonicalName)}`,
101
+ renderAliases(entity.aliases),
102
+ `confidence: ${entity.confidence}`,
103
+ "status: draft",
104
+ "domain: []",
105
+ `first_seen: ${entity.now}`,
106
+ `last_updated: ${entity.now}`,
107
+ `signal_count: ${entity.signalCount}`,
108
+ "---",
109
+ "",
110
+ `# ${entity.canonicalName}`,
111
+ "",
112
+ ].join("\n");
113
+ }
114
+ const OWNED_KEYS = new Set(["aliases", "signal_count", "last_updated"]);
115
+ /**
116
+ * Replace one top-level key in a raw frontmatter block. The key's value may
117
+ * span several lines (a block sequence); continuation lines are the ones that
118
+ * start with whitespace or `- `. Returns null when the key is absent.
119
+ */
120
+ function replaceTopLevelKey(lines, key, rendered) {
121
+ const start = lines.findIndex((line) => line === `${key}:` || line.startsWith(`${key}: `) || line.startsWith(`${key}:\t`));
122
+ if (start === -1)
123
+ return null;
124
+ let end = start + 1;
125
+ while (end < lines.length && (/^\s/.test(lines[end]) || lines[end].startsWith("- ") || lines[end] === "-"))
126
+ end += 1;
127
+ return [...lines.slice(0, start), ...rendered.split("\n"), ...lines.slice(end)];
128
+ }
129
+ /**
130
+ * Append alias items to a block-style `aliases:` sequence, leaving the existing
131
+ * item lines untouched. Falls back to re-rendering the whole key for flow style
132
+ * (`aliases: []`, `aliases: [a, b]`). Returns null when the key is absent.
133
+ */
134
+ function appendAliases(lines, all, added) {
135
+ const start = lines.findIndex((line) => line === "aliases:" || /^aliases:\s*(#.*)?$/.test(line));
136
+ if (start === -1)
137
+ return replaceTopLevelKey(lines, "aliases", renderAliases(all));
138
+ let end = start + 1;
139
+ while (end < lines.length && (/^\s/.test(lines[end]) || lines[end].startsWith("- ") || lines[end] === "-"))
140
+ end += 1;
141
+ const firstItem = lines.slice(start + 1, end).find((line) => /^\s*- /.test(line) || /^\s*-$/.test(line));
142
+ const indent = firstItem ? /^(\s*)/.exec(firstItem)[1] : " ";
143
+ return [...lines.slice(0, end), ...added.map((alias) => `${indent}- ${quote(alias)}`), ...lines.slice(end)];
144
+ }
145
+ /**
146
+ * Patch an existing, valid entity file. Aliases are only appended, and
147
+ * `signal_count` never goes down. When nothing but `last_updated` would
148
+ * change, the file is returned unchanged so a rerun writes nothing.
149
+ */
150
+ export function patchEntityFile(content, patch) {
151
+ const match = FRONTMATTER_RE.exec(content);
152
+ const parsed = parseEntityFile(content);
153
+ if (!match || !parsed)
154
+ return { ok: false, reason: "unparseable" };
155
+ const before = validateEntityFrontmatter(parsed.frontmatter);
156
+ if (typeof before === "string")
157
+ return { ok: false, reason: `invalid_existing:${before}` };
158
+ const aliases = [...before.aliases];
159
+ for (const alias of patch.addAliases ?? []) {
160
+ if (!aliases.includes(alias))
161
+ aliases.push(alias);
162
+ }
163
+ const signalCount = Math.max(before.signal_count, patch.signalCount ?? 0);
164
+ const aliasesChanged = aliases.length !== before.aliases.length;
165
+ const countChanged = signalCount !== before.signal_count;
166
+ if (!aliasesChanged && !countChanged && !patch.bodyChanged)
167
+ return { ok: true, content, changed: false };
168
+ const newline = content.includes("\r\n") ? "\r\n" : "\n";
169
+ let lines = match[1].split(/\r?\n/);
170
+ if (aliasesChanged)
171
+ lines = appendAliases(lines, aliases, aliases.slice(before.aliases.length));
172
+ if (lines && countChanged)
173
+ lines = replaceTopLevelKey(lines, "signal_count", `signal_count: ${signalCount}`);
174
+ if (lines)
175
+ lines = replaceTopLevelKey(lines, "last_updated", `last_updated: ${patch.lastUpdated}`);
176
+ if (!lines)
177
+ return { ok: false, reason: "missing_owned_key" };
178
+ const head = content.slice(0, content.indexOf(match[1]));
179
+ const tail = content.slice(content.indexOf(match[1]) + match[1].length);
180
+ const next = `${head}${lines.join(newline)}${tail}`;
181
+ const check = checkPatched(parsed.frontmatter, next, aliases, signalCount, patch.lastUpdated);
182
+ if (check)
183
+ return { ok: false, reason: check };
184
+ return { ok: true, content: next, changed: true };
185
+ }
186
+ /** The patched file must validate and keep every key it does not own. */
187
+ function checkPatched(original, next, aliases, signalCount, lastUpdated) {
188
+ const reparsed = parseEntityFile(next);
189
+ if (!reparsed)
190
+ return "patch_unparseable";
191
+ if (typeof validateEntityFrontmatter(reparsed.frontmatter) === "string")
192
+ return "patch_invalid";
193
+ const keys = new Set([...Object.keys(original), ...Object.keys(reparsed.frontmatter)]);
194
+ for (const key of keys) {
195
+ if (OWNED_KEYS.has(key))
196
+ continue;
197
+ if (!isDeepStrictEqual(original[key], reparsed.frontmatter[key]))
198
+ return `patch_changed_key:${key}`;
199
+ }
200
+ if (!isDeepStrictEqual(reparsed.frontmatter.aliases, aliases))
201
+ return "patch_aliases";
202
+ if (reparsed.frontmatter.signal_count !== signalCount)
203
+ return "patch_signal_count";
204
+ if (reparsed.frontmatter.last_updated !== lastUpdated)
205
+ return "patch_last_updated";
206
+ return null;
207
+ }
208
+ /** hq-pro `previewSignalContent`: first 80 characters, one line. */
209
+ export function previewSignalContent(canonicalContent) {
210
+ const collapsed = canonicalContent.replace(/\s+/g, " ").trim();
211
+ return collapsed.length > 80 ? collapsed.slice(0, 80) : collapsed;
212
+ }
213
+ /**
214
+ * The company-relative target of a back-reference link. The gardener writes
215
+ * `../../signals/...` and hq-cli writes `../../../signals/...`; both name the
216
+ * same signal.
217
+ */
218
+ function backRefTarget(href) {
219
+ return href.replace(/^(?:\.\.\/)+/, "");
220
+ }
221
+ /**
222
+ * hq-pro `appendBackRefsToBody`: add `- Referenced in [<type>: <preview>](<href>)`
223
+ * lines to `## Relationships`, keyed on the link target. Existing lines are
224
+ * never removed or reworded.
225
+ */
226
+ export function appendBackRefs(fullContent, refs) {
227
+ if (refs.length === 0)
228
+ return { content: fullContent, added: 0 };
229
+ const fmMatch = /^(---\r?\n[\s\S]*?\r?\n---\r?\n?)([\s\S]*)$/.exec(fullContent);
230
+ if (!fmMatch)
231
+ return { content: fullContent, added: 0 };
232
+ const frontmatter = fmMatch[1];
233
+ const body = fmMatch[2];
234
+ const sectionMatch = /(^|\n)(##\s+Relationships\s*\n)([\s\S]*?)(?=\n##\s|\s*$)/.exec(body);
235
+ const existing = new Set();
236
+ if (sectionMatch) {
237
+ for (const link of sectionMatch[3].matchAll(/\]\(([^)]+)\)/g))
238
+ existing.add(backRefTarget(link[1]));
239
+ }
240
+ const fresh = [];
241
+ for (const ref of refs) {
242
+ if (existing.has(backRefTarget(ref.href)))
243
+ continue;
244
+ existing.add(backRefTarget(ref.href));
245
+ fresh.push(ref);
246
+ }
247
+ if (fresh.length === 0)
248
+ return { content: fullContent, added: 0 };
249
+ const newLines = fresh.map((ref) => `- Referenced in [${ref.signalType}: ${ref.preview}](${ref.href})`).join("\n");
250
+ let nextBody;
251
+ if (sectionMatch) {
252
+ const before = body.slice(0, sectionMatch.index + sectionMatch[1].length);
253
+ const header = sectionMatch[2];
254
+ const section = sectionMatch[3].replace(/\s+$/, "");
255
+ const after = body.slice(sectionMatch.index + sectionMatch[0].length);
256
+ nextBody = `${before}${header}${section ? `${section}\n${newLines}` : newLines}\n${after}`;
257
+ }
258
+ else {
259
+ const trimmed = body.replace(/\s+$/, "");
260
+ nextBody = `${trimmed.length > 0 ? `${trimmed}\n\n` : "\n"}## Relationships\n\n${newLines}\n`;
261
+ }
262
+ return { content: `${frontmatter}${nextBody}`, added: fresh.length };
263
+ }
264
+ //# sourceMappingURL=entity-file.js.map
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Entity identity rules shared with the hq-pro ontology.
3
+ *
4
+ * `normalizeEntityName` and `toSlug` are ports of hq-pro
5
+ * `src/ontology/entity-normalize.ts` and `src/ontology/file-writer.ts`, and
6
+ * `entityId` is hq-pro `generateEntityId`. The read model validates every
7
+ * synced entity file with these rules, so they must match byte for byte.
8
+ */
9
+ /** The entity types this writer creates or updates. */
10
+ export declare const WRITABLE_ENTITY_TYPES: readonly ["person", "project", "company", "concept"];
11
+ export type WritableEntityType = (typeof WRITABLE_ENTITY_TYPES)[number];
12
+ export declare function isWritableEntityType(value: unknown): value is WritableEntityType;
13
+ /**
14
+ * hq-pro `normalizeEntityName`. Organisation types drop punctuation and
15
+ * trailing corporate suffixes; every other type keeps `+` and `#` so `C` and
16
+ * `C++` stay distinct.
17
+ */
18
+ export declare function normalizeEntityName(name: string, type?: string): string;
19
+ /** hq-pro `toSlug`: the entity file name without `.md`. */
20
+ export declare function toSlug(canonicalName: string): string;
21
+ /** hq-pro `generateEntityId`: sha256 of `<type>:<canonical_name>`. */
22
+ export declare function entityId(type: string, canonicalName: string): string;
23
+ /**
24
+ * The canonical form of a new name: NFC, trimmed, internal whitespace
25
+ * collapsed to one space. The id is hashed from exactly this string.
26
+ */
27
+ export declare function canonicalizeName(raw: string): string;
28
+ /** A normalised name with its spaces removed (`recall ai` → `recallai`). */
29
+ export declare function squash(normalized: string): string;
30
+ /**
31
+ * True when a name must not become a new entity: empty, one character, only
32
+ * digits, or a single common word. `github` and `slack` are here because the
33
+ * common-noun use dominates; the real companies already exist in the tree
34
+ * and are matched by tiers 1 and 2 before this filter runs.
35
+ */
36
+ export declare function isRejectedName(normalized: string): boolean;
37
+ //# sourceMappingURL=identity.d.ts.map
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Entity identity rules shared with the hq-pro ontology.
3
+ *
4
+ * `normalizeEntityName` and `toSlug` are ports of hq-pro
5
+ * `src/ontology/entity-normalize.ts` and `src/ontology/file-writer.ts`, and
6
+ * `entityId` is hq-pro `generateEntityId`. The read model validates every
7
+ * synced entity file with these rules, so they must match byte for byte.
8
+ */
9
+ import { createHash } from "node:crypto";
10
+ /** The entity types this writer creates or updates. */
11
+ export const WRITABLE_ENTITY_TYPES = ["person", "project", "company", "concept"];
12
+ export function isWritableEntityType(value) {
13
+ return typeof value === "string" && WRITABLE_ENTITY_TYPES.includes(value);
14
+ }
15
+ const CORPORATE_SUFFIXES = new Set([
16
+ "inc", "incorporated", "llc", "llp", "lp", "ltd", "limited", "corp", "corporation",
17
+ "co", "company", "gmbh", "ag", "sa", "sas", "srl", "bv", "nv", "plc", "pty", "pte",
18
+ "kg", "oy", "ab", "as", "spa",
19
+ ]);
20
+ const ORG_NORMALIZED_TYPES = new Set([
21
+ "company", "contact", "deal", "contract", "invoice", "account", "journal-entry",
22
+ "ar-invoice", "ap-bill",
23
+ ]);
24
+ /**
25
+ * hq-pro `normalizeEntityName`. Organisation types drop punctuation and
26
+ * trailing corporate suffixes; every other type keeps `+` and `#` so `C` and
27
+ * `C++` stay distinct.
28
+ */
29
+ export function normalizeEntityName(name, type) {
30
+ if (type !== undefined && !ORG_NORMALIZED_TYPES.has(type)) {
31
+ return name.toLowerCase().replace(/[^a-z0-9+#]+/g, " ").trim();
32
+ }
33
+ const base = name.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
34
+ if (base === "")
35
+ return "";
36
+ let tokens = base.split(" ").filter((token) => token !== "");
37
+ while (tokens.length > 1 && CORPORATE_SUFFIXES.has(tokens[tokens.length - 1])) {
38
+ tokens = tokens.slice(0, -1);
39
+ }
40
+ return tokens.join(" ");
41
+ }
42
+ /** hq-pro `toSlug`: the entity file name without `.md`. */
43
+ export function toSlug(canonicalName) {
44
+ return canonicalName
45
+ .toLowerCase()
46
+ .replace(/\s+/g, "-")
47
+ .replace(/[^a-z0-9-]/g, "")
48
+ .replace(/-+/g, "-")
49
+ .replace(/^-|-$/g, "");
50
+ }
51
+ /** hq-pro `generateEntityId`: sha256 of `<type>:<canonical_name>`. */
52
+ export function entityId(type, canonicalName) {
53
+ return createHash("sha256").update(`${type}:${canonicalName}`).digest("hex");
54
+ }
55
+ /**
56
+ * The canonical form of a new name: NFC, trimmed, internal whitespace
57
+ * collapsed to one space. The id is hashed from exactly this string.
58
+ */
59
+ export function canonicalizeName(raw) {
60
+ return raw.normalize("NFC").replace(/\s+/g, " ").trim();
61
+ }
62
+ /** A normalised name with its spaces removed (`recall ai` → `recallai`). */
63
+ export function squash(normalized) {
64
+ return normalized.replace(/ /g, "");
65
+ }
66
+ /**
67
+ * Names that are never created as entities. The gardener's false positives on
68
+ * indigo were mostly single common words linked as entities ("metadata",
69
+ * "For", "AI", "Index", "personal").
70
+ */
71
+ const STOPWORDS = new Set([
72
+ "a", "an", "the", "and", "or", "for", "to", "of", "in", "on", "at", "by", "with", "from",
73
+ "ai", "api", "app", "apps", "data", "index", "metadata", "personal", "private", "public",
74
+ "user", "users", "team", "teams", "company", "companies", "project", "projects", "product",
75
+ "system", "service", "services", "tool", "tools", "file", "files", "doc", "docs", "readme",
76
+ "config", "configuration", "test", "tests", "update", "updates", "new", "note", "notes",
77
+ "plan", "summary", "overview", "architecture", "conventions", "environment", "setup",
78
+ "meeting", "meetings", "customer", "customers", "client", "clients", "agent", "agents",
79
+ "worker", "workers", "session", "sessions", "task", "tasks", "issue", "issues", "bug",
80
+ "bugs", "feature", "features", "release", "releases", "email",
81
+ ]);
82
+ /**
83
+ * True when a name must not become a new entity: empty, one character, only
84
+ * digits, or a single common word. `github` and `slack` are here because the
85
+ * common-noun use dominates; the real companies already exist in the tree
86
+ * and are matched by tiers 1 and 2 before this filter runs.
87
+ */
88
+ export function isRejectedName(normalized) {
89
+ if (normalized.length < 2)
90
+ return true;
91
+ if (/^[0-9 ]+$/.test(normalized))
92
+ return true;
93
+ return STOPWORDS.has(normalized);
94
+ }
95
+ //# sourceMappingURL=identity.js.map
@@ -0,0 +1,124 @@
1
+ /**
2
+ * `hq core ontology plan`: pick this run's inputs.
3
+ *
4
+ * Runs in the detached checkpoint sibling. It resolves the flag, finds the
5
+ * company's changed knowledge and signal files with a content-hash ledger,
6
+ * applies the per-run budget, and writes `<runDir>/ontology-plan.json` for the
7
+ * backend model to read. The model only fills a candidates file; everything
8
+ * that affects identity, scope and cost stays in code.
9
+ */
10
+ import { type OntologyCompany } from "./company.js";
11
+ export declare const ONTOLOGY_FLAG = "checkpoint.ontology";
12
+ export declare const SHARED_INPUTS_FLAG = "checkpoint.ontology.shared-inputs";
13
+ export declare const PLAN_FILE = "ontology-plan.json";
14
+ export declare const CANDIDATES_FILE = "ontology-candidates.json";
15
+ export declare const REPORT_FILE = "ontology-report.json";
16
+ export declare const PLAN_SCHEMA = "hq-checkpoint-ontology-plan.v1";
17
+ export declare const CANDIDATES_SCHEMA = "hq-checkpoint-ontology-candidates.v1";
18
+ export declare const LEDGER_SCHEMA = "hq-checkpoint-ontology-ledger.v1";
19
+ export declare const BUDGET: {
20
+ readonly maxInputs: 20;
21
+ readonly maxTokens: 60000;
22
+ readonly chunkTokens: 12000;
23
+ };
24
+ export type PendingReason = "session-touched" | "shared" | "first-run-gap" | "missing-refs";
25
+ export interface LedgerEntry {
26
+ sha256: string;
27
+ size: number;
28
+ mtime_ms: number;
29
+ state: "seen" | "pending" | "done";
30
+ reason?: PendingReason;
31
+ /** Next chunk to process for a file larger than one chunk. */
32
+ next_chunk?: number;
33
+ run?: string;
34
+ }
35
+ export interface Ledger {
36
+ schema: typeof LEDGER_SCHEMA;
37
+ company: string;
38
+ /** Latest entity `last_updated` when the ledger was seeded (design 14.3). */
39
+ boundary: string | null;
40
+ entries: Record<string, LedgerEntry>;
41
+ }
42
+ export interface PlanInput {
43
+ /** Candidate `input` value: the company-relative path, `#<chunk>` when chunked, or `session`. */
44
+ id: string;
45
+ kind: "knowledge" | "signal" | "session";
46
+ /** Company-relative path of the file to read; null for `session` (its text is in the plan). */
47
+ path: string | null;
48
+ audience: "company" | "session";
49
+ reason: PendingReason;
50
+ sha256: string | null;
51
+ est_tokens: number;
52
+ /** 1-based inclusive line range to read, for a file larger than one chunk. */
53
+ lines: [number, number] | null;
54
+ chunk: {
55
+ index: number;
56
+ count: number;
57
+ } | null;
58
+ /** For signals: `entity_refs` items with no entity file yet. */
59
+ missing_refs?: string[];
60
+ }
61
+ export interface OntologyPlan {
62
+ schema: typeof PLAN_SCHEMA;
63
+ company: string;
64
+ run_id: string;
65
+ created_at: string;
66
+ first_run: boolean;
67
+ boundary: string | null;
68
+ shared_inputs: boolean;
69
+ inputs: PlanInput[];
70
+ /** The session's summary, decisions and learnings, for the `session` input. */
71
+ session_text: string | null;
72
+ pending_after_run: number;
73
+ budget: {
74
+ max_inputs: number;
75
+ max_tokens: number;
76
+ chunk_tokens: number;
77
+ planned_tokens: number;
78
+ };
79
+ candidates_file: string;
80
+ instructions: string[];
81
+ }
82
+ export interface CheckpointPayloadLike {
83
+ company?: unknown;
84
+ files?: unknown;
85
+ summary?: unknown;
86
+ decisions?: unknown;
87
+ learnings?: unknown;
88
+ insights?: unknown;
89
+ }
90
+ export type FlagReader = (flagKey: string, company: OntologyCompany) => Promise<boolean>;
91
+ export interface PlanOptions {
92
+ hqRoot: string;
93
+ runDir: string;
94
+ payload: CheckpointPayloadLike;
95
+ readFlag: FlagReader;
96
+ now?: Date;
97
+ }
98
+ export type PlanOutcome = {
99
+ status: "skipped";
100
+ reason: string;
101
+ } | {
102
+ status: "planned";
103
+ plan: OntologyPlan;
104
+ planPath: string;
105
+ };
106
+ export declare function ledgerPath(hqRoot: string, company: OntologyCompany): string;
107
+ export declare function readLedger(hqRoot: string, company: OntologyCompany): Ledger | null;
108
+ export declare function writeLedger(hqRoot: string, company: OntologyCompany, ledger: Ledger): void;
109
+ /** Company-relative paths of the payload's touched files that are inside this company. */
110
+ export declare function sessionTouched(hqRoot: string, company: OntologyCompany, files: unknown): Set<string>;
111
+ /** 1-based line ranges, each at most `chunkTokens` tokens (bytes / 4). */
112
+ export declare function chunkLines(content: string, chunkTokens: number): Array<[number, number]>;
113
+ /**
114
+ * Update the ledger with the current tree and return it. New or changed files
115
+ * become pending when this machine should process them (design 14.1) and
116
+ * seen otherwise. The first run seeds from the entity boundary (design 14.3).
117
+ */
118
+ export declare function refreshLedger(company: OntologyCompany, existing: Ledger | null, touched: Set<string>, sharedInputs: boolean): {
119
+ ledger: Ledger;
120
+ firstRun: boolean;
121
+ };
122
+ /** Plan this run. Never throws for a skip; returns the reason instead. */
123
+ export declare function planOntologyRun(options: PlanOptions): Promise<PlanOutcome>;
124
+ //# sourceMappingURL=plan.d.ts.map