tldr-experts 0.3.0 → 0.3.1

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.
Files changed (32) hide show
  1. package/CHANGELOG.md +994 -0
  2. package/README.md +26 -7
  3. package/dist/hooks/answer-capture.js +20 -11
  4. package/dist/hooks/budget-gate.js +28 -8
  5. package/dist/hooks/chunk-0z27twdk.js +159 -0
  6. package/dist/hooks/{chunk-0bt6yb2q.js → chunk-39zh2e44.js} +10 -1
  7. package/dist/hooks/{chunk-1zwcxd3f.js → chunk-3g61yg59.js} +1 -1
  8. package/dist/hooks/chunk-3t91gvpp.js +193 -0
  9. package/dist/hooks/{chunk-kw4tffzf.js → chunk-458wgg9j.js} +30 -24
  10. package/dist/hooks/{chunk-j234zf0t.js → chunk-9kkm6q0t.js} +4 -165
  11. package/dist/hooks/{chunk-t8tdv11p.js → chunk-afamdvyn.js} +1 -1
  12. package/dist/hooks/{chunk-a8p2rc94.js → chunk-s5qsb4k6.js} +1 -1
  13. package/dist/hooks/{chunk-g395gk7e.js → chunk-sznsenee.js} +1 -1
  14. package/dist/hooks/{chunk-y0jdr3et.js → chunk-xpxe94qe.js} +23 -10
  15. package/dist/hooks/{chunk-azctppjh.js → chunk-ybacnpxd.js} +4 -1
  16. package/dist/hooks/{chunk-x98qs959.js → chunk-ytvmc5ns.js} +19 -22
  17. package/dist/hooks/chunk-zdxgragg.js +1655 -0
  18. package/dist/hooks/{chunk-p274ckxv.js → chunk-ztczwtj0.js} +6 -1
  19. package/dist/hooks/claim-sources.js +9 -9
  20. package/dist/hooks/dod-gate.js +12 -37
  21. package/dist/hooks/no-reask.js +9 -9
  22. package/dist/hooks/session-start.js +176 -422
  23. package/dist/hooks/statusline.js +14 -11
  24. package/dist/tldrx.js +12029 -6330
  25. package/package.json +1 -1
  26. package/plugin/.claude-plugin/plugin.json +1 -1
  27. package/plugin/skills/tldrx/SKILL.md +16 -3
  28. package/stages/build/stage.md +5 -0
  29. package/templates/budget.yml +5 -0
  30. package/dist/hooks/chunk-ae6bkfs5.js +0 -0
  31. package/dist/hooks/chunk-sdjnnmzz.js +0 -497
  32. package/dist/hooks/chunk-t56k6146.js +0 -14
package/README.md CHANGED
@@ -42,10 +42,17 @@ dependencies, so an installed `tldrx` needs only Node; Bun builds it. Full walkt
42
42
  ## How much human is in the loop
43
43
 
44
44
  Every stage ends at a gate; what you choose is **who closes it**. `human` waits for `tldrx approve`.
45
- `auto` lets the harness close it, and only when all five conditions hold: the stage's checks pass, its
46
- phase has no open question, the spend is inside both the stage and phase ceilings, the stage did not
47
- fail, and the claim-sources validator reports nothing. Any one failing falls back to the human gate
48
- and says which one and what it measured.
45
+ `auto` lets the harness close it, and only when all **seven** conditions hold: the stage's checks pass,
46
+ its phase has no open question, the spend is inside both the stage and phase ceilings, the stage did not
47
+ fail, the claim-sources validator reports nothing — and, on a Build stage, every story in the plan
48
+ reached `done` **and** the epic branch changed nothing the run never declared it would touch. Any one
49
+ failing falls back to the human gate and says which one and what it measured.
50
+
51
+ `agent` is the third policy, and the strongest: those same seven, plus no budget decision taken while
52
+ the stage ran, plus a validated **evidence note** the agent signed — a checklist whose own bullets each
53
+ carry a `[src: …]` that resolves. It arrives by choice (`--gates plan:agent`), never by default, and it
54
+ falls through to a person on an open question, a moved ceiling, work outside the declared boundary, or
55
+ its own refusal. See [10 Unattended mode](docs/guide/10-unattended-mode.md).
49
56
 
50
57
  | Scope | what | how | plan | build | watch |
51
58
  |---|---|---|---|---|---|
@@ -59,11 +66,20 @@ and says which one and what it measured.
59
66
  | `security-patch` | auto | auto | — | human | human |
60
67
  | `migration` | auto | auto | auto | human | human |
61
68
 
69
+ `--parallel <n>` on `next` / `run auto` builds that many of a wave's stories at once
70
+ (merges still land in the wave's listed order; default 1 is unchanged).
71
+
72
+ A scope with `—` under `plan` does not run the Plan phase, and Build writes the one story that
73
+ decision implies (`04-build/implicit-plan.yml`) from your What handoff rather than refusing;
74
+ `run status` says `plan: implicit (scope skips Plan)`. A `03-plan/` you write yourself always wins.
75
+
62
76
  Those are the shipped defaults, and every scope keeps at least one human gate. Override per run with
63
77
  `--gates <stage,stage>` — **the list is the human gates** — or `--gates all|none`. When the machine
64
78
  signs something it should not have, `tldrx reject --stage <phase>/<stage> --note "…"` revokes it, moves
65
- the cursor back and marks the later stages `stale`. What an auto gate cannot do:
66
- [`docs/guide/03-runs-and-gates.md`](docs/guide/03-runs-and-gates.md).
79
+ the cursor back and marks the later stages `stale`. When it is one BUILD STORY you disagree with — a
80
+ story two reviewers refused, which is terminal for the rest of the run —
81
+ `tldrx story reopen <id> --note "…"` gives that one story another run of attempts and nothing else.
82
+ What an auto gate cannot do: [`docs/guide/03-runs-and-gates.md`](docs/guide/03-runs-and-gates.md).
67
83
 
68
84
  ## What you see while it runs
69
85
 
@@ -143,7 +159,9 @@ The guide, in `docs/guide/`: [1 Quick start](docs/guide/01-quick-start.md) ·
143
159
  [5 Seeds and triage](docs/guide/05-seeds-and-triage.md) (`--seed`, `--from`, splitting a big seed) ·
144
160
  [6 Budgets and cost](docs/guide/06-budgets-and-cost.md) · [7 Claude Code](docs/guide/07-claude-code.md) (plugin, hooks, `/tldrx`) ·
145
161
  [8 CLI reference](docs/guide/08-cli-reference.md) (every command, flag and exit code) ·
146
- [9 Troubleshooting](docs/guide/09-troubleshooting.md) (every refusal, and the move that clears it).
162
+ [9 Troubleshooting](docs/guide/09-troubleshooting.md) (every refusal, and the move that clears it) ·
163
+ [10 Unattended mode](docs/guide/10-unattended-mode.md) (`attended_by: host`, `gates_policy: agent`, the
164
+ review handshake, the fix list, decision cards).
147
165
  Design docs: [`docs/concept.md`](docs/concept.md) (why) · [`docs/spec.md`](docs/spec.md) (the schemas, and §7's
148
166
  open decisions) · [`docs/ROADMAP.md`](docs/ROADMAP.md) (next) · [`CHANGELOG.md`](CHANGELOG.md) (shipped) ·
149
167
  [`docs/dashboard-model.md`](docs/dashboard-model.md).
@@ -157,6 +175,7 @@ back on the registry is 0.3.0.
157
175
 
158
176
  | Version | Date | Status | Contains |
159
177
  |---|---|---|---|
178
+ | 0.3.1 | 2026-08-31 | `alpha` | Unattended mode (gates_policy agent, review handshake, fixlist, decision cards, dual economy), 6 contact fixes from the first feature-scope runs, colored init, training repair round |
160
179
  | 0.3.0 | 2026-08-30 | `alpha` | expert training with provenance, auto gates with an undo, `tldrx status`, seed triage, the token economy (context ledger, `max_reads`, `cost`, `estimate`), `install --claude`, `interview`, the ticket mirror, `--help` with flags and exit codes |
161
180
  | 0.2.0 | 2026-08-29 | `alpha` | Build executor (worktree + branch per story, epic branches, DoD gate, reviewer), Watch cards, live dashboard |
162
181
  | 0.1.0 | 2026-08-29 | `alpha` | greenfield `init --stack` + `run new --seed`, story/epic/waves schemas, `tldrx budget show\|raise`, sections must hold list items |
@@ -1,29 +1,29 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- FactsStore,
4
- MAX_FACT_CHARS
5
- } from "./chunk-x98qs959.js";
3
+ FactsStore
4
+ } from "./chunk-ytvmc5ns.js";
6
5
  import {
7
6
  parseHookInput,
8
7
  readStdin
9
- } from "./chunk-a8p2rc94.js";
8
+ } from "./chunk-s5qsb4k6.js";
10
9
  import {
11
10
  EventLog
12
- } from "./chunk-azctppjh.js";
11
+ } from "./chunk-ybacnpxd.js";
12
+ import"./chunk-9kkm6q0t.js";
13
13
  import {
14
+ MAX_FACT_CHARS,
14
15
  detectAnswered,
15
16
  parseQuestions,
16
17
  recordAnswer,
17
18
  replaceBlock,
18
19
  serializeQuestions
19
- } from "./chunk-j234zf0t.js";
20
- import"./chunk-t56k6146.js";
21
- import"./chunk-g395gk7e.js";
22
- import"./chunk-0bt6yb2q.js";
20
+ } from "./chunk-3t91gvpp.js";
21
+ import"./chunk-sznsenee.js";
22
+ import"./chunk-39zh2e44.js";
23
23
  import {
24
24
  PROJECT_WORK_DIR,
25
25
  factsPath
26
- } from "./chunk-p274ckxv.js";
26
+ } from "./chunk-ztczwtj0.js";
27
27
 
28
28
  // src/hooks/answer-capture.ts
29
29
  import { existsSync as existsSync2 } from "fs";
@@ -99,8 +99,15 @@ function nowRfc3339() {
99
99
 
100
100
  // src/core/answers/captureAnswers.ts
101
101
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
102
+ var TRUNCATION_MARK = " …";
102
103
  function factTextFor(title, answer) {
103
- return `${title} — ${answer}`.slice(0, MAX_FACT_CHARS);
104
+ const whole = `${title} — ${answer}`;
105
+ if (whole.length <= MAX_FACT_CHARS)
106
+ return whole;
107
+ return `${whole.slice(0, MAX_FACT_CHARS - TRUNCATION_MARK.length)}${TRUNCATION_MARK}`;
108
+ }
109
+ function factWasTruncated(title, answer) {
110
+ return `${title} — ${answer}`.length > MAX_FACT_CHARS;
104
111
  }
105
112
  function captureAnswers(questionsPath, ctx) {
106
113
  if (!existsSync(questionsPath))
@@ -114,8 +121,10 @@ function captureAnswers(questionsPath, ctx) {
114
121
  FactsStore.update(factsPath(ctx.root), (store) => {
115
122
  for (const block of answered) {
116
123
  const area = block.metadata?.area ?? "unscoped";
124
+ const truncated = factWasTruncated(block.title, block.answer);
117
125
  const fact = store.append({
118
126
  fact: factTextFor(block.title, block.answer),
127
+ ...truncated ? { truncated: true } : {},
119
128
  area,
120
129
  repos: [],
121
130
  kind: "answer",
@@ -8,34 +8,40 @@ import {
8
8
  readPayload,
9
9
  runHook,
10
10
  toolInput
11
- } from "./chunk-1zwcxd3f.js";
12
- import"./chunk-a8p2rc94.js";
11
+ } from "./chunk-3g61yg59.js";
12
+ import"./chunk-s5qsb4k6.js";
13
13
  import {
14
14
  asRunBudget,
15
15
  currentActor,
16
16
  cursorStage,
17
+ economyFor,
18
+ isHostTokens,
17
19
  loadRunView,
18
20
  newestActiveRun,
19
21
  nowRfc3339,
20
22
  raiseCommand,
23
+ remainingWork,
21
24
  shortBy,
22
25
  validateRunBudget,
23
26
  wouldExceed
24
- } from "./chunk-sdjnnmzz.js";
27
+ } from "./chunk-zdxgragg.js";
25
28
  import {
26
29
  EventLog
27
- } from "./chunk-azctppjh.js";
30
+ } from "./chunk-ybacnpxd.js";
31
+ import"./chunk-458wgg9j.js";
32
+ import"./chunk-0z27twdk.js";
28
33
  import {
29
34
  noteDeprecations
30
- } from "./chunk-t56k6146.js";
31
- import"./chunk-0bt6yb2q.js";
35
+ } from "./chunk-3t91gvpp.js";
36
+ import"./chunk-sznsenee.js";
37
+ import"./chunk-39zh2e44.js";
32
38
  import {
33
39
  PROJECT_WORK_DIR,
34
40
  findWorkspaceRoot,
35
41
  locateWork,
36
42
  parseYaml,
37
43
  stageYamlPath
38
- } from "./chunk-p274ckxv.js";
44
+ } from "./chunk-ztczwtj0.js";
39
45
 
40
46
  // src/hooks/budget-gate.ts
41
47
  import { existsSync as existsSync2, readFileSync as readFileSync2, statSync } from "node:fs";
@@ -102,9 +108,23 @@ await runHook("budget-gate", async () => {
102
108
  if (budget === null)
103
109
  failClosed(command, `${view.dir}/budget.yml is missing or unreadable`);
104
110
  const stage = cursorStage(view);
105
- const estimate = estimateFor(command, stage?.budget_usd ?? stageBudgetFromLibrary(root, view.cursor.stage));
111
+ const declared = stage?.budget_usd ?? stageBudgetFromLibrary(root, view.cursor.stage);
112
+ const work = declared === null ? null : remainingWork({
113
+ runDir: view.dir,
114
+ phaseId: view.cursor.phase,
115
+ stageBudgetUsd: declared,
116
+ stageSpentUsd: stage?.cost_usd ?? 0,
117
+ perAgentMaxUsd: budget.per_agent_max_usd,
118
+ maxUsd: null,
119
+ economy: economyFor(budget, view.cursor.phase)
120
+ });
121
+ const estimate = estimateFor(command, work === null ? null : work.usd);
106
122
  if (estimate <= 0)
107
123
  return;
124
+ if (isHostTokens(budget, view.cursor.phase)) {
125
+ process.stderr.write(`tldrx hook budget-gate: ${view.cursor.phase} is priced in \`host-tokens\` — ` + "no dollar ceiling to enforce here; `tldrx next` refuses a headless spawn on it.\n");
126
+ return;
127
+ }
108
128
  const decision = wouldExceed(budget, view.cursor.phase, estimate);
109
129
  if (!decision.blocked)
110
130
  return;
@@ -0,0 +1,159 @@
1
+ import {
2
+ isRecord
3
+ } from "./chunk-39zh2e44.js";
4
+ import {
5
+ parseYaml
6
+ } from "./chunk-ztczwtj0.js";
7
+
8
+ // src/core/schemas/planCommon.ts
9
+ var STORY_ID_RE = /^S\d{1,4}$/;
10
+ var WAVE_ID_RE = /^W\d{1,3}$/;
11
+ var MAX_LIST_ITEMS = 64;
12
+ var MAX_ITEM_CHARS = 512;
13
+ var MAX_WAVES = 32;
14
+ var MAX_STORIES_PER_WAVE = 32;
15
+ var MAX_PLAN_STORIES = 200;
16
+ function requireStringList(value, path, issues, rules = {}) {
17
+ if (value === undefined)
18
+ return [];
19
+ if (!Array.isArray(value)) {
20
+ issues.push({ path, message: `expected a list, got ${describe(value)}` });
21
+ return [];
22
+ }
23
+ const max = rules.max ?? MAX_LIST_ITEMS;
24
+ if (value.length > max) {
25
+ issues.push({ path, message: `${value.length} items exceeds the ${max} cap` });
26
+ }
27
+ if (rules.nonEmpty === true && value.length === 0) {
28
+ issues.push({ path, message: "must not be empty" });
29
+ }
30
+ const seen = new Set;
31
+ const out = [];
32
+ value.forEach((item, i) => {
33
+ const at = `${path}[${i}]`;
34
+ if (typeof item !== "string") {
35
+ issues.push({ path: at, message: `expected a string, got ${describe(item)}` });
36
+ return;
37
+ }
38
+ if (item.trim() === "") {
39
+ issues.push({ path: at, message: "must not be blank" });
40
+ return;
41
+ }
42
+ if (item.length > MAX_ITEM_CHARS) {
43
+ issues.push({ path: at, message: `${item.length} characters exceeds the ${MAX_ITEM_CHARS} cap` });
44
+ }
45
+ if (rules.pattern !== undefined && !rules.pattern.test(item)) {
46
+ issues.push({ path: at, message: `expected ${rules.patternName ?? String(rules.pattern)}, got \`${item}\`` });
47
+ return;
48
+ }
49
+ if (rules.unique === true && seen.has(item)) {
50
+ issues.push({ path: at, message: `\`${item}\` is listed twice` });
51
+ return;
52
+ }
53
+ seen.add(item);
54
+ out.push(item);
55
+ });
56
+ return out;
57
+ }
58
+ function requirePattern(value, pattern, patternName, path, issues) {
59
+ if (value === undefined)
60
+ return;
61
+ if (typeof value !== "string") {
62
+ issues.push({ path, message: `expected a string, got ${describe(value)}` });
63
+ return;
64
+ }
65
+ if (!pattern.test(value)) {
66
+ issues.push({ path, message: `expected ${patternName}, got \`${value}\`` });
67
+ }
68
+ }
69
+ function requireVersion1(doc, issues) {
70
+ if (doc.version !== undefined && doc.version !== 1) {
71
+ issues.push({ path: "version", message: `unknown schema version ${String(doc.version)} (expected 1)` });
72
+ }
73
+ }
74
+ function describe(value) {
75
+ if (value === null)
76
+ return "null";
77
+ if (Array.isArray(value))
78
+ return "a list";
79
+ if (isRecord(value))
80
+ return "a mapping";
81
+ return typeof value;
82
+ }
83
+
84
+ // src/core/schemas/frontMatter.ts
85
+ var FENCE = "---";
86
+ function splitFrontMatter(text) {
87
+ const lines = text.split(`
88
+ `);
89
+ if ((lines[0] ?? "").trimEnd() !== FENCE) {
90
+ return { present: false, raw: "", body: text, bodyLine: 1 };
91
+ }
92
+ for (let i = 1;i < lines.length; i++) {
93
+ if ((lines[i] ?? "").trimEnd() !== FENCE)
94
+ continue;
95
+ return {
96
+ present: true,
97
+ raw: lines.slice(1, i).join(`
98
+ `),
99
+ body: lines.slice(i + 1).join(`
100
+ `),
101
+ bodyLine: i + 2
102
+ };
103
+ }
104
+ return { present: false, raw: "", body: text, bodyLine: 1 };
105
+ }
106
+ function parseFrontMatter(text) {
107
+ const frontMatter = splitFrontMatter(text);
108
+ if (!frontMatter.present) {
109
+ return {
110
+ frontMatter,
111
+ doc: null,
112
+ issue: {
113
+ path: "",
114
+ message: "no YAML front matter — the file must open with `---` and close the block with `---`"
115
+ }
116
+ };
117
+ }
118
+ try {
119
+ return { frontMatter, doc: parseYaml(frontMatter.raw), issue: null };
120
+ } catch (error) {
121
+ return {
122
+ frontMatter,
123
+ doc: null,
124
+ issue: { path: "", message: `front matter is not valid YAML: ${message(error)}` }
125
+ };
126
+ }
127
+ }
128
+ function message(error) {
129
+ return error instanceof Error ? error.message.split(`
130
+ `)[0] ?? "parse error" : String(error);
131
+ }
132
+ // src/core/schemas/story.ts
133
+ var FENCE_OPEN_RE = /^\s*```+\s*dod\s*$/i;
134
+ var FENCE_CLOSE_RE = /^\s*```+\s*$/;
135
+ function parseDodBlock(text) {
136
+ const commands = [];
137
+ let present = false;
138
+ let inside = false;
139
+ for (const line of text.split(`
140
+ `)) {
141
+ if (inside) {
142
+ if (FENCE_CLOSE_RE.test(line)) {
143
+ inside = false;
144
+ continue;
145
+ }
146
+ const command = line.trim();
147
+ if (command !== "" && !command.startsWith("#"))
148
+ commands.push(command);
149
+ continue;
150
+ }
151
+ if (FENCE_OPEN_RE.test(line)) {
152
+ present = true;
153
+ inside = true;
154
+ }
155
+ }
156
+ return { present, commands };
157
+ }
158
+
159
+ export { STORY_ID_RE, WAVE_ID_RE, MAX_WAVES, MAX_STORIES_PER_WAVE, MAX_PLAN_STORIES, requireStringList, requirePattern, requireVersion1, parseFrontMatter, parseDodBlock };
@@ -67,6 +67,15 @@ function requireString(value, path, issues) {
67
67
  issues.push({ path, message: `expected a string, got ${describe(value)}` });
68
68
  }
69
69
  }
70
+ function requireRecord(value, path, issues) {
71
+ if (value === undefined)
72
+ return false;
73
+ if (!isRecord(value)) {
74
+ issues.push({ path, message: `expected a mapping, got ${describe(value)}` });
75
+ return false;
76
+ }
77
+ return true;
78
+ }
70
79
  function asDocument(input, issues) {
71
80
  if (!isRecord(input)) {
72
81
  issues.push({ path: "", message: `expected a mapping at the document root, got ${describe(input)}` });
@@ -85,4 +94,4 @@ function describeValue(value) {
85
94
  return typeof value === "number" || typeof value === "boolean" ? String(value) : describe(value);
86
95
  }
87
96
 
88
- export { result, requireVersion, isRecord, requireKeys, requireEnum, requireArray, requireNumber, requireString, asDocument };
97
+ export { result, requireVersion, isRecord, requireKeys, requireEnum, requireArray, requireNumber, requireString, requireRecord, asDocument };
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  parseHookInput,
3
3
  readStdin
4
- } from "./chunk-a8p2rc94.js";
4
+ } from "./chunk-s5qsb4k6.js";
5
5
 
6
6
  // src/hooks/lib/decide.ts
7
7
  function deny(reason) {
@@ -0,0 +1,193 @@
1
+ import {
2
+ parseSrcToken
3
+ } from "./chunk-sznsenee.js";
4
+
5
+ // src/core/text/questions.ts
6
+ var REQUIRED_METADATA_KEYS = ["id", "status", "area", "asked_by", "asked_at"];
7
+ var HEADING_RE = /^##\s+(Q\d{1,6})\s+·\s+(.+?)\s*$/;
8
+ var METADATA_RE = /^<!--\s*(.*?)\s*-->$/;
9
+ var WHY_RE = /^Why asked:\s*(.*)$/;
10
+ var OPTION_RE = /^-\s+([A-E])\)\s*(.*)$/;
11
+ var ANSWER_RE = /^\[Answer\]:[ \t]*(\S.*)$/;
12
+ var ANSWER_SLOT_RE = /^\[Answer\]:/;
13
+ var FOOTER_KEYS = ["answered_by", "answered_at", "fact"];
14
+ function parseQuestions(text) {
15
+ const trailingNewline = text.endsWith(`
16
+ `);
17
+ const body = trailingNewline ? text.slice(0, -1) : text;
18
+ const lines = body === "" ? [] : body.split(`
19
+ `);
20
+ const preamble = [];
21
+ const blocks = [];
22
+ let pending = null;
23
+ const close = () => {
24
+ if (pending === null)
25
+ return;
26
+ blocks.push(buildBlock(pending.start + 1, pending.lines));
27
+ pending = null;
28
+ };
29
+ for (let i = 0;i < lines.length; i++) {
30
+ const line = lines[i] ?? "";
31
+ if (HEADING_RE.test(line)) {
32
+ close();
33
+ pending = { start: i, lines: [line] };
34
+ continue;
35
+ }
36
+ if (pending === null)
37
+ preamble.push(line);
38
+ else
39
+ pending.lines.push(line);
40
+ }
41
+ close();
42
+ return { preamble, blocks, trailingNewline };
43
+ }
44
+ function buildBlock(startLine, lines) {
45
+ const heading = HEADING_RE.exec(lines[0] ?? "");
46
+ const id = heading?.[1] ?? "";
47
+ const title = heading?.[2] ?? "";
48
+ let metadata = null;
49
+ let metadataIndex = -1;
50
+ let whyAsked = null;
51
+ let whySrc = null;
52
+ const options = [];
53
+ let answer = "";
54
+ let answerIndex = -1;
55
+ let footer = null;
56
+ for (let i = 1;i < lines.length; i++) {
57
+ const line = lines[i] ?? "";
58
+ const comment = METADATA_RE.exec(line);
59
+ if (comment !== null && comment[1] !== undefined) {
60
+ const pairs = parsePipeComment(comment[1]);
61
+ if (metadata === null && pairs.some(([k]) => k === "id")) {
62
+ metadata = toMetadata(pairs);
63
+ metadataIndex = i;
64
+ } else if (footer === null && pairs.some(([k]) => k === "answered_by")) {
65
+ footer = toFooter(pairs);
66
+ }
67
+ continue;
68
+ }
69
+ const why = WHY_RE.exec(line);
70
+ if (why !== null && whyAsked === null) {
71
+ whyAsked = why[1] ?? "";
72
+ whySrc = parseSrcToken(line);
73
+ continue;
74
+ }
75
+ const option = OPTION_RE.exec(line);
76
+ if (option !== null && option[1] !== undefined && option[2] !== undefined) {
77
+ options.push({ letter: option[1], text: option[2] });
78
+ continue;
79
+ }
80
+ if (ANSWER_SLOT_RE.test(line) && answerIndex === -1) {
81
+ answerIndex = i;
82
+ const captured = ANSWER_RE.exec(line);
83
+ answer = (captured?.[1] ?? "").trim();
84
+ }
85
+ }
86
+ return {
87
+ id,
88
+ title,
89
+ metadata,
90
+ metadataIndex,
91
+ whyAsked,
92
+ whySrc,
93
+ options,
94
+ answer,
95
+ answerIndex,
96
+ footer,
97
+ startLine,
98
+ lines: [...lines]
99
+ };
100
+ }
101
+ function parsePipeComment(inner) {
102
+ const pairs = [];
103
+ for (const part of inner.split("|")) {
104
+ const colon = part.indexOf(":");
105
+ if (colon === -1)
106
+ continue;
107
+ pairs.push([part.slice(0, colon).trim(), part.slice(colon + 1).trim()]);
108
+ }
109
+ return pairs;
110
+ }
111
+ function toMetadata(pairs) {
112
+ const map = new Map(pairs);
113
+ const extra = pairs.filter(([k]) => !REQUIRED_METADATA_KEYS.includes(k));
114
+ return {
115
+ id: map.get("id") ?? "",
116
+ status: map.get("status") ?? "",
117
+ area: map.get("area") ?? "",
118
+ asked_by: map.get("asked_by") ?? "",
119
+ asked_at: map.get("asked_at") ?? "",
120
+ extra
121
+ };
122
+ }
123
+ function toFooter(pairs) {
124
+ const map = new Map(pairs);
125
+ return {
126
+ answered_by: map.get("answered_by") ?? "",
127
+ answered_at: map.get("answered_at") ?? "",
128
+ fact: map.get("fact") ?? ""
129
+ };
130
+ }
131
+ function serializeQuestions(doc) {
132
+ const out = [...doc.preamble];
133
+ for (const block of doc.blocks)
134
+ out.push(...block.lines);
135
+ const text = out.join(`
136
+ `);
137
+ return doc.trailingNewline ? `${text}
138
+ ` : text;
139
+ }
140
+ function detectAnswered(blocks) {
141
+ return blocks.filter((b) => b.metadata?.status === "open" && b.answer !== "");
142
+ }
143
+ function openBlocks(blocks) {
144
+ return blocks.filter((b) => b.metadata?.status === "open");
145
+ }
146
+ function recordAnswer(block, footer) {
147
+ const lines = [...block.lines];
148
+ if (block.metadataIndex >= 0) {
149
+ const line = lines[block.metadataIndex] ?? "";
150
+ lines[block.metadataIndex] = line.replace(/(\|\s*status:\s*)open(\s*\|)/, "$1answered$2");
151
+ }
152
+ const footerLine = `<!-- ${FOOTER_KEYS.map((k) => `${k}: ${footer[k]}`).join(" | ")} -->`;
153
+ const insertAt = block.answerIndex >= 0 ? block.answerIndex + 1 : lines.length;
154
+ lines.splice(insertAt, 0, footerLine);
155
+ const metadata = block.metadata === null ? null : { ...block.metadata, status: "answered" };
156
+ return { ...block, lines, metadata, footer };
157
+ }
158
+ function replaceBlock(doc, block) {
159
+ return {
160
+ ...doc,
161
+ blocks: doc.blocks.map((b) => b.id === block.id ? block : b)
162
+ };
163
+ }
164
+
165
+ // src/core/facts/Fact.ts
166
+ var FACT_KINDS = ["answer", "observed", "derived"];
167
+ var FACT_CONFIDENCES = ["measured", "inferred", "stated"];
168
+ var MAX_FACTS = 5000;
169
+ var MAX_FACT_CHARS = 2000;
170
+ function isRetired(fact) {
171
+ return fact.retired !== null && fact.retired.at !== null;
172
+ }
173
+ function factNumber(id) {
174
+ return Number.parseInt(id.slice(1), 10);
175
+ }
176
+ function formatFactId(n) {
177
+ return `F${String(n).padStart(3, "0")}`;
178
+ }
179
+
180
+ // src/core/schemas/deprecationNotice.ts
181
+ var said = new Set;
182
+ function noteDeprecations(file, outcome) {
183
+ for (const note of outcome.deprecations ?? []) {
184
+ const line = `${file}: ${note}`;
185
+ if (said.has(line))
186
+ continue;
187
+ said.add(line);
188
+ process.stderr.write(`${line}
189
+ `);
190
+ }
191
+ }
192
+
193
+ export { FACT_KINDS, FACT_CONFIDENCES, MAX_FACTS, MAX_FACT_CHARS, isRetired, factNumber, formatFactId, noteDeprecations, parseQuestions, serializeQuestions, detectAnswered, openBlocks, recordAnswer, replaceBlock };