ahead-pi 0.2.0 → 0.2.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 (40) hide show
  1. package/README.md +13 -3
  2. package/generated/product-change/ai-audit.md +6 -3
  3. package/generated/product-change/ai-review.md +6 -3
  4. package/generated/product-change/decision.md +6 -3
  5. package/generated/product-change/define.md +6 -3
  6. package/generated/product-change/deploy.md +6 -3
  7. package/generated/product-change/human-review.md +6 -3
  8. package/generated/product-change/implement.md +11 -4
  9. package/generated/product-change/manifest.json +2 -2
  10. package/generated/product-change/options.md +6 -3
  11. package/generated/product-change/outcome.md +6 -3
  12. package/generated/product-change/plan.md +6 -3
  13. package/generated/product-change/questions.md +6 -3
  14. package/generated/product-change/research.md +6 -3
  15. package/generated/product-change/verify.md +6 -3
  16. package/generated/reference/CONSTITUTION.md +43 -0
  17. package/generated/reference/docs/acceptable-ai-use.md +217 -0
  18. package/generated/reference/docs/design/debugging-and-operations.md +119 -0
  19. package/generated/reference/docs/design/executable-workflows.md +110 -0
  20. package/generated/reference/docs/design/process-taxonomy.md +144 -0
  21. package/generated/reference/docs/engineering-practice.md +163 -0
  22. package/generated/reference/docs/evidence/evidence-standard.md +123 -0
  23. package/generated/reference/docs/evidence/research-map.md +98 -0
  24. package/generated/reference/docs/rationale.md +210 -0
  25. package/generated/reference/docs/references/pragmatic-programmer-page-index.md +113 -0
  26. package/generated/reference/docs/references/submitted-engineering-notes.md +306 -0
  27. package/generated/reference/docs/releasing-pi.md +89 -0
  28. package/generated/reference/docs/workflows/README.md +142 -0
  29. package/generated/reference/docs/workflows/corrective-debugging.md +203 -0
  30. package/generated/reference/docs/workflows/decision.md +149 -0
  31. package/generated/reference/docs/workflows/internal-improvement.md +207 -0
  32. package/generated/reference/docs/workflows/investigation.md +159 -0
  33. package/generated/reference/docs/workflows/operational-stabilization.md +185 -0
  34. package/generated/reference/docs/workflows/product-change.md +205 -0
  35. package/generated/reference/index.json +180 -0
  36. package/package.json +4 -2
  37. package/src/guidance.ts +6 -4
  38. package/src/index.ts +128 -0
  39. package/src/reference-viewer.ts +71 -0
  40. package/src/reference.ts +56 -0
@@ -0,0 +1,180 @@
1
+ {
2
+ "generated_from": [
3
+ "CONSTITUTION.md",
4
+ "docs/acceptable-ai-use.md",
5
+ "docs/design/debugging-and-operations.md",
6
+ "docs/design/executable-workflows.md",
7
+ "docs/design/process-taxonomy.md",
8
+ "docs/engineering-practice.md",
9
+ "docs/evidence/evidence-standard.md",
10
+ "docs/evidence/research-map.md",
11
+ "docs/rationale.md",
12
+ "docs/references/pragmatic-programmer-page-index.md",
13
+ "docs/references/submitted-engineering-notes.md",
14
+ "docs/releasing-pi.md",
15
+ "docs/workflows/README.md",
16
+ "docs/workflows/corrective-debugging.md",
17
+ "docs/workflows/decision.md",
18
+ "docs/workflows/internal-improvement.md",
19
+ "docs/workflows/investigation.md",
20
+ "docs/workflows/operational-stabilization.md",
21
+ "docs/workflows/product-change.md"
22
+ ],
23
+ "references": [
24
+ {
25
+ "id": "constitution",
26
+ "path": "CONSTITUTION.md",
27
+ "title": "AHEAD Constitution",
28
+ "summary": "This constitution records the durable principles of **Assisted Human Engineering and Development**. Workflows and tools may evolve. These principles define what makes them AHEAD.",
29
+ "phases": [
30
+ "*"
31
+ ]
32
+ },
33
+ {
34
+ "id": "acceptable-ai-use",
35
+ "path": "docs/acceptable-ai-use.md",
36
+ "title": "Acceptable AI Use in AHEAD",
37
+ "summary": "This policy is binding for AHEAD pilot workflows. The [AHEAD Constitution](../CONSTITUTION.md) is the highest authority; this policy interprets its human-ownership boundaries; workflow profiles apply both and may narrow AI permissions for a",
38
+ "phases": [
39
+ "*"
40
+ ]
41
+ },
42
+ {
43
+ "id": "design:debugging-and-operations",
44
+ "path": "docs/design/debugging-and-operations.md",
45
+ "title": "Debugging and Operational Investigation",
46
+ "summary": "The minimal [corrective-debugging](../workflows/corrective-debugging.md) and [operational-stabilization](../workflows/operational-stabilization.md) profiles translate this discussion into pilotable flows. This document retains the reasoning",
47
+ "phases": []
48
+ },
49
+ {
50
+ "id": "design:executable-workflows",
51
+ "path": "docs/design/executable-workflows.md",
52
+ "title": "Executable AHEAD Workflows",
53
+ "summary": "The executable layer makes AHEAD workflow state durable and makes selected human/AI boundaries enforceable across integrations. It does not turn judgment into a checklist or make workflow artifacts proof of understanding.",
54
+ "phases": [
55
+ "*"
56
+ ]
57
+ },
58
+ {
59
+ "id": "design:process-taxonomy",
60
+ "path": "docs/design/process-taxonomy.md",
61
+ "title": "AHEAD Process Taxonomy",
62
+ "summary": "AHEAD should not create a workflow for every issue label. “Security,” “performance,” “data,” “incident,” and “technical debt” often describe risk, domain, urgency, or cause—not the kind of reasoning needed to complete the work.",
63
+ "phases": []
64
+ },
65
+ {
66
+ "id": "engineering-practice",
67
+ "path": "docs/engineering-practice.md",
68
+ "title": "AHEAD Engineering Practice",
69
+ "summary": "AHEAD is not only a sequence of AI gates. It is a way of practicing engineering. This guide distills general habits from the submitted starting list into a smaller set of principles that can be remembered, applied, and evaluated.",
70
+ "phases": [
71
+ "*"
72
+ ]
73
+ },
74
+ {
75
+ "id": "evidence:evidence-standard",
76
+ "path": "docs/evidence/evidence-standard.md",
77
+ "title": "AHEAD Evidence Standard",
78
+ "summary": "AHEAD should be supported by the best available evidence without claiming more certainty than software-engineering research can provide. A credible methodology must distinguish evidence for a descriptive claim from evidence that a particula",
79
+ "phases": [
80
+ "research",
81
+ "questions",
82
+ "decision",
83
+ "plan",
84
+ "ai-review",
85
+ "human-review",
86
+ "verify",
87
+ "ai-audit",
88
+ "outcome"
89
+ ]
90
+ },
91
+ {
92
+ "id": "evidence:research-map",
93
+ "path": "docs/evidence/research-map.md",
94
+ "title": "AHEAD Research Map",
95
+ "summary": "This document maps current AHEAD design claims to available evidence. It is not a bibliography of everything related to software engineering. It focuses on claims that materially shape the methodology.",
96
+ "phases": []
97
+ },
98
+ {
99
+ "id": "rationale",
100
+ "path": "docs/rationale.md",
101
+ "title": "Why AHEAD",
102
+ "summary": "AI can make software engineering faster. It can search broadly, synthesize information, identify omissions, generate alternatives, produce routine code, explain unfamiliar systems, and review more material than a person can inspect unaided.",
103
+ "phases": [
104
+ "*"
105
+ ]
106
+ },
107
+ {
108
+ "id": "references:pragmatic-programmer-page-index",
109
+ "path": "docs/references/pragmatic-programmer-page-index.md",
110
+ "title": "*The Pragmatic Programmer* Page Index",
111
+ "summary": "This index preserves the page-level provenance from the original AHEAD engineering-practice notes for:",
112
+ "phases": []
113
+ },
114
+ {
115
+ "id": "references:submitted-engineering-notes",
116
+ "path": "docs/references/submitted-engineering-notes.md",
117
+ "title": "Submitted Engineering Notes",
118
+ "summary": "This document preserves submitted engineering tips, checklists, and practices that are intentionally condensed in the [AHEAD Engineering Practice](../engineering-practice.md) guide. It is a source record, not a claim that every item is an e",
119
+ "phases": []
120
+ },
121
+ {
122
+ "id": "releasing-pi",
123
+ "path": "docs/releasing-pi.md",
124
+ "title": "Releasing the Pi Extension",
125
+ "summary": "- npm package: `ahead-pi` - package source: `integrations/pi` - release workflow: `.github/workflows/publish-pi.yml` - release tag: `pi-v<package-version>` - stable npm dist-tag: `latest` - prerelease npm dist-tag: `next` - registry: `https",
126
+ "phases": []
127
+ },
128
+ {
129
+ "id": "workflows:readme",
130
+ "path": "docs/workflows/README.md",
131
+ "title": "AHEAD Pilot Workflows",
132
+ "summary": "These six minimal workflow profiles are meant to be used on real engineering work before AHEAD builds a workflow engine, editor extension, or CI enforcement. They are deliberately small. The pilot should reveal which phases, gates, and reco",
133
+ "phases": []
134
+ },
135
+ {
136
+ "id": "workflows:corrective-debugging",
137
+ "path": "docs/workflows/corrective-debugging.md",
138
+ "title": "Corrective Debugging Workflow",
139
+ "summary": "Use this flow when observed behavior conflicts with intended behavior and the dominant work is causal diagnosis plus correction. It ends with a verified correction or an explicit human acceptance of remaining uncertainty.",
140
+ "phases": []
141
+ },
142
+ {
143
+ "id": "workflows:decision",
144
+ "path": "docs/workflows/decision.md",
145
+ "title": "Decision Workflow",
146
+ "summary": "Use this flow when the deliverable is an accountable human choice among alternatives: architecture, buy versus build, technology selection, platform direction, policy, or another consequential course.",
147
+ "phases": []
148
+ },
149
+ {
150
+ "id": "workflows:internal-improvement",
151
+ "path": "docs/workflows/internal-improvement.md",
152
+ "title": "Internal Improvement Workflow",
153
+ "summary": "Use this flow when externally required behavior should remain invariant while an internal quality improves: maintainability, comprehensibility, performance, reliability margin, operability, testability, cost, or preventive risk reduction.",
154
+ "phases": []
155
+ },
156
+ {
157
+ "id": "workflows:investigation",
158
+ "path": "docs/workflows/investigation.md",
159
+ "title": "Investigation Workflow",
160
+ "summary": "Use this flow when the primary deliverable is knowledge or reduced uncertainty and no intervention has yet been selected. It may answer feasibility, causal, capacity, vendor, architectural, or technical questions.",
161
+ "phases": []
162
+ },
163
+ {
164
+ "id": "workflows:operational-stabilization",
165
+ "path": "docs/workflows/operational-stabilization.md",
166
+ "title": "Operational Stabilization Workflow",
167
+ "summary": "Use this flow when a live system is outside an acceptable operating state and the immediate outcome is restoration, stabilization, or demonstrated convergence. It covers code and non-code causes such as reconciliation storms, configuration ",
168
+ "phases": []
169
+ },
170
+ {
171
+ "id": "workflows:product-change",
172
+ "path": "docs/workflows/product-change.md",
173
+ "title": "Product Change Workflow",
174
+ "summary": "Use this flow when the dominant outcome is new, changed, adapted, migrated, or retired externally meaningful behavior. It ends with verified intended behavior and an accountable human outcome decision.",
175
+ "phases": [
176
+ "*"
177
+ ]
178
+ }
179
+ ]
180
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ahead-pi",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "AHEAD workflow enforcement and context for Pi",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -49,10 +49,12 @@
49
49
  },
50
50
  "peerDependencies": {
51
51
  "@earendil-works/pi-coding-agent": "*",
52
+ "@earendil-works/pi-tui": "*",
52
53
  "typebox": "*"
53
54
  },
54
55
  "devDependencies": {
55
- "@earendil-works/pi-coding-agent": "0.80.6",
56
+ "@earendil-works/pi-coding-agent": "^0.84.1",
57
+ "@earendil-works/pi-tui": "^0.84.1",
56
58
  "@types/node": "24.12.4",
57
59
  "typebox": "1.1.38",
58
60
  "typescript": "5.9.3"
package/src/guidance.ts CHANGED
@@ -110,8 +110,8 @@ const guides: Record<string, PhaseGuide> = {
110
110
  },
111
111
  implement: {
112
112
  objective: "Produce a change the engineer understands and can defend.",
113
- human: "Own the implementation, understand every lasting change, run the planned checks, and record deviations.",
114
- ai: "Assist within the approved plan, explain changes, run authorized checks, and surface reasons to revisit earlier phases.",
113
+ human: "Make the first attempt, ask questions freely, own the implementation, understand every lasting change, run the planned checks, and record deviations.",
114
+ ai: "Coach, explain, help diagnose, and suggest bounded next steps within the approved plan. Do not turn a question into taking over the implementation.",
115
115
  artifactPrompts: {
116
116
  changeset: [
117
117
  "What exact commit, branch, pull request, or diff identifies the current change?",
@@ -252,7 +252,9 @@ export function nextAction(state: RunState, workflow: WorkflowDefinition): Guide
252
252
  ? "ai"
253
253
  : "human";
254
254
  if (actor === "human") {
255
- const label = state.phase.id === "human-review"
255
+ const label = state.phase.id === "implement" && artifact.kind === "changeset"
256
+ ? "Implement first, then record the exact changeset"
257
+ : state.phase.id === "human-review"
256
258
  ? "Independent reviewer records the current human review"
257
259
  : `Write ${artifact.title}`;
258
260
  return { actor, label, artifactKind: artifact.kind };
@@ -323,7 +325,7 @@ export function buildWidgetLines(run: Run, state: RunState, workflow: WorkflowDe
323
325
  `AI: ${guide.ai}`,
324
326
  `Required: ${checklist}`,
325
327
  `Next (${action.actor === "human" ? "you" : "AI"}): ${action.label}`,
326
- "Run /ahead for the guided action.",
328
+ "Run /ahead for the guided action · /ahead-guide for framework docs.",
327
329
  ];
328
330
  }
329
331
 
package/src/index.ts CHANGED
@@ -14,6 +14,13 @@ import {
14
14
  phaseGuide,
15
15
  phasePosition,
16
16
  } from "./guidance.js";
17
+ import {
18
+ findReference,
19
+ loadReferenceIndex,
20
+ readReference,
21
+ relevantReferences,
22
+ } from "./reference.js";
23
+ import { showReferenceViewer } from "./reference-viewer.js";
17
24
  import { humanActor, projectRoot, RunStore } from "./storage.js";
18
25
  import type { Actor, Capability, EventAction, Run, RunState } from "./types.js";
19
26
 
@@ -38,6 +45,9 @@ const RecordArtifactParams = Type.Object({
38
45
  kind: Type.String({ description: "Artifact kind permitted for AI in the active phase" }),
39
46
  content: Type.String({ description: "Complete Markdown artifact content", maxLength: 100_000 }),
40
47
  });
48
+ const ReferenceParams = Type.Object({
49
+ topic: Type.Optional(Type.String({ description: "Reference id, path, or title; omit to list phase-relevant references" })),
50
+ });
41
51
 
42
52
  export default function aheadExtension(pi: ExtensionAPI): void {
43
53
  pi.registerCommand("ahead", {
@@ -47,6 +57,13 @@ export default function aheadExtension(pi: ExtensionAPI): void {
47
57
  }),
48
58
  });
49
59
 
60
+ pi.registerCommand("ahead-guide", {
61
+ description: "Read the AHEAD framework guidance relevant to the active phase",
62
+ handler: async (args, ctx) => command(ctx, async () => {
63
+ await showAheadGuide(ctx, args);
64
+ }),
65
+ });
66
+
50
67
  pi.registerCommand("ahead-start", {
51
68
  description: "Advanced: start a Product Change run directly",
52
69
  handler: async (args, ctx) => command(ctx, async () => {
@@ -137,6 +154,7 @@ export default function aheadExtension(pi: ExtensionAPI): void {
137
154
  ctx.ui.notify(
138
155
  [
139
156
  "/ahead [title] — enter, resume, or act in guided AHEAD mode",
157
+ "/ahead-guide [topic] — read the applicable AHEAD framework Markdown",
140
158
  "",
141
159
  "Once started, the repository run remains in AHEAD mode until an accountable human closes the outcome.",
142
160
  "Use normal conversation to think and work with AI. Run /ahead whenever you want the next valid action.",
@@ -165,6 +183,34 @@ export default function aheadExtension(pi: ExtensionAPI): void {
165
183
  },
166
184
  });
167
185
 
186
+ pi.registerTool({
187
+ name: "ahead_get_reference",
188
+ label: "AHEAD framework reference",
189
+ description: "List or read packaged AHEAD Constitution, philosophy, acceptable-use, engineering-practice, workflow, and evidence Markdown.",
190
+ promptSnippet: "Retrieve relevant AHEAD framework guidance when the phase or policy is unclear.",
191
+ parameters: ReferenceParams,
192
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
193
+ return toolResult(async () => {
194
+ const run = await storeFor(ctx).loadCurrent();
195
+ const phase = run && !(await enginePromise).deriveState(run).closed
196
+ ? (await enginePromise).deriveState(run).phase.id
197
+ : undefined;
198
+ if (!params.topic?.trim()) {
199
+ const index = await loadReferenceIndex();
200
+ return {
201
+ phase: phase ?? null,
202
+ recommended: await relevantReferences(phase),
203
+ available: index.references.map(({ id, title, path }) => ({ id, title, path })),
204
+ instruction: "Request one reference by id, path, or title. Load only what is relevant.",
205
+ };
206
+ }
207
+ const entry = await findReference(params.topic);
208
+ if (!entry) throw new Error(`No packaged AHEAD reference matches ${params.topic}`);
209
+ return { reference: entry, content: await readReference(entry) };
210
+ });
211
+ },
212
+ });
213
+
168
214
  pi.registerTool({
169
215
  name: "ahead_record_artifact",
170
216
  label: "Record AHEAD artifact",
@@ -277,6 +323,8 @@ export default function aheadExtension(pi: ExtensionAPI): void {
277
323
  "- Never author a human-owned artifact, make a human decision, accept a gate, transition the run, approve a change, or claim accountability.",
278
324
  "- When required AI-owned work is ready, record it with ahead_record_artifact and explain what the human must validate or decide.",
279
325
  "- Treat AI review findings as hypotheses. Independent human review remains required for lasting engineering changes.",
326
+ "- Humans may ask questions at any phase. During implementation, help them understand or solve the problem without taking over; if their first attempt or current model is missing, ask for it.",
327
+ "- When AHEAD policy or rationale is unclear, use ahead_get_reference to retrieve only the applicable packaged Markdown.",
280
328
  ].join("\n");
281
329
  return { systemPrompt: `${event.systemPrompt}\n\n${phaseInstructions}\n\n${liveContext}\n` };
282
330
  });
@@ -378,6 +426,7 @@ async function openAheadMode(pi: ExtensionAPI, args: string, ctx: ExtensionComma
378
426
  state.allowed_ai_capabilities.length > 0
379
427
  && action.actor !== "ai"
380
428
  && !missingRequired.some((artifact) => artifact.actor === "ai")
429
+ && state.phase.id !== "implement"
381
430
  ) {
382
431
  actions.push({
383
432
  label: `Ask AI to assist · ${state.phase.title}`,
@@ -385,6 +434,13 @@ async function openAheadMode(pi: ExtensionAPI, args: string, ctx: ExtensionComma
385
434
  });
386
435
  }
387
436
 
437
+ if (state.phase.id === "implement") {
438
+ actions.push({
439
+ label: "Ask AI for help understanding or solving a problem",
440
+ run: async () => askImplementationQuestion(pi, ctx, state),
441
+ });
442
+ }
443
+
388
444
  if (state.return_targets.length > 0) {
389
445
  actions.push({
390
446
  label: "Return to an earlier phase",
@@ -392,6 +448,11 @@ async function openAheadMode(pi: ExtensionAPI, args: string, ctx: ExtensionComma
392
448
  });
393
449
  }
394
450
 
451
+ actions.push({
452
+ label: "Read AHEAD framework guidance for this phase",
453
+ run: async () => showAheadGuide(ctx, ""),
454
+ });
455
+
395
456
  actions.push({
396
457
  label: "Explain this phase and its expectations",
397
458
  run: async () => {
@@ -416,6 +477,73 @@ async function openAheadMode(pi: ExtensionAPI, args: string, ctx: ExtensionComma
416
477
  if (chosen) await chosen.run();
417
478
  }
418
479
 
480
+ async function askImplementationQuestion(
481
+ pi: ExtensionAPI,
482
+ ctx: ExtensionCommandContext,
483
+ state: RunState,
484
+ ): Promise<void> {
485
+ if (!ctx.hasUI) throw new Error("Implementation coaching requires interactive or RPC UI support");
486
+ const question = await ctx.ui.editor(
487
+ "AHEAD implementation help · human first",
488
+ [
489
+ "## What are you trying to understand or solve?",
490
+ "",
491
+ "",
492
+ "## What do you currently think is happening or should happen?",
493
+ "",
494
+ "",
495
+ "## What have you tried or inspected so far?",
496
+ "",
497
+ "",
498
+ "## What kind of help would be useful?",
499
+ "",
500
+ "<!-- Ask for explanation, a hint, competing approaches, debugging help, or a bounded suggestion. -->",
501
+ "",
502
+ ].join("\n"),
503
+ );
504
+ if (!question?.trim()) return;
505
+ pi.sendUserMessage([
506
+ `AHEAD mode: help me with this ${state.phase.title} question while I remain the implementer.`,
507
+ "Use my current model and first attempt below. Help me understand or solve the problem with questions, explanation, evidence, hints, and bounded next steps.",
508
+ "Do not convert this question into autonomous implementation or author my human-owned records. If I later request a bounded mechanical edit, explain it so I can inspect and own it.",
509
+ "",
510
+ question.trim(),
511
+ ].join("\n"));
512
+ }
513
+
514
+ async function showAheadGuide(ctx: ExtensionCommandContext, requestedTopic: string): Promise<void> {
515
+ if (!ctx.hasUI) throw new Error("Reading AHEAD framework guidance requires interactive or RPC UI support");
516
+ const run = await storeFor(ctx).loadCurrent();
517
+ const phase = run && !(await enginePromise).deriveState(run).closed
518
+ ? (await enginePromise).deriveState(run).phase.id
519
+ : undefined;
520
+ const index = await loadReferenceIndex();
521
+ let entry = requestedTopic.trim() && requestedTopic.trim().toLowerCase() !== "all"
522
+ ? await findReference(requestedTopic)
523
+ : undefined;
524
+
525
+ if (requestedTopic.trim() && requestedTopic.trim().toLowerCase() !== "all" && !entry) {
526
+ throw new Error(`No packaged AHEAD reference matches ${requestedTopic.trim()}`);
527
+ }
528
+
529
+ if (!entry) {
530
+ const recommended = requestedTopic.trim().toLowerCase() === "all"
531
+ ? index.references
532
+ : await relevantReferences(phase);
533
+ const browseAll = "Browse all packaged AHEAD Markdown";
534
+ const selected = await ctx.ui.select(
535
+ phase ? `AHEAD guidance · ${phase}` : "AHEAD framework guidance",
536
+ [...recommended.map((candidate) => candidate.title), ...(recommended.length < index.references.length ? [browseAll] : [])],
537
+ );
538
+ if (!selected) return;
539
+ if (selected === browseAll) return showAheadGuide(ctx, "all");
540
+ entry = recommended.find((candidate) => candidate.title === selected);
541
+ }
542
+ if (!entry) return;
543
+
544
+ await showReferenceViewer(ctx, `AHEAD reference · ${entry.title}`, await readReference(entry));
545
+ }
546
+
419
547
  async function startRun(ctx: ExtensionCommandContext, requestedTitle: string): Promise<Run | undefined> {
420
548
  const engine = await enginePromise;
421
549
  const store = storeFor(ctx);
@@ -0,0 +1,71 @@
1
+ import {
2
+ getMarkdownTheme,
3
+ type ExtensionCommandContext,
4
+ } from "@earendil-works/pi-coding-agent";
5
+ import {
6
+ Markdown,
7
+ matchesKey,
8
+ truncateToWidth,
9
+ visibleWidth,
10
+ } from "@earendil-works/pi-tui";
11
+
12
+ export async function showReferenceViewer(
13
+ ctx: ExtensionCommandContext,
14
+ title: string,
15
+ content: string,
16
+ ): Promise<void> {
17
+ await ctx.ui.custom<void>((tui, theme, _keybindings, done) => {
18
+ const markdown = new Markdown(content.trim(), 0, 0, getMarkdownTheme());
19
+ let scrollOffset = 0;
20
+ let pageSize = 10;
21
+ let totalLines = 0;
22
+
23
+ const component = {
24
+ render(width: number): string[] {
25
+ const innerWidth = Math.max(20, width - 2);
26
+ const contentWidth = Math.max(18, innerWidth - 2);
27
+ const rendered = markdown.render(contentWidth);
28
+ totalLines = rendered.length;
29
+ pageSize = Math.max(6, Math.min(30, tui.terminal.rows - 7));
30
+ const maxOffset = Math.max(0, totalLines - pageSize);
31
+ scrollOffset = Math.min(scrollOffset, maxOffset);
32
+ const visible = rendered.slice(scrollOffset, scrollOffset + pageSize);
33
+ const remaining = Math.max(0, totalLines - pageSize - scrollOffset);
34
+ const border = (value: string) => theme.fg("border", value);
35
+ const pad = (value: string): string => {
36
+ const truncated = truncateToWidth(value, innerWidth, "…", true);
37
+ return `${truncated}${" ".repeat(Math.max(0, innerWidth - visibleWidth(truncated)))}`;
38
+ };
39
+ const lines = [
40
+ border(`╭${"─".repeat(innerWidth)}╮`),
41
+ `${border("│")}${pad(` ${theme.fg("accent", theme.bold(title))}`)}${border("│")}`,
42
+ `${border("│")}${pad(theme.fg("dim", ` ↑ ${scrollOffset} lines · ↓ ${remaining} lines`))}${border("│")}`,
43
+ ];
44
+ for (const line of visible) lines.push(`${border("│")}${pad(` ${line}`)}${border("│")}`);
45
+ for (let index = visible.length; index < pageSize; index += 1) {
46
+ lines.push(`${border("│")}${pad("")}${border("│")}`);
47
+ }
48
+ lines.push(
49
+ `${border("│")}${pad(theme.fg("dim", " ↑↓ scroll · PgUp/PgDn page · Home/End jump · Esc close"))}${border("│")}`,
50
+ border(`╰${"─".repeat(innerWidth)}╯`),
51
+ );
52
+ return lines;
53
+ },
54
+ invalidate(): void {
55
+ markdown.invalidate();
56
+ },
57
+ handleInput(data: string): void {
58
+ const maxOffset = Math.max(0, totalLines - pageSize);
59
+ if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) done();
60
+ else if (matchesKey(data, "up")) scrollOffset = Math.max(0, scrollOffset - 1);
61
+ else if (matchesKey(data, "down")) scrollOffset = Math.min(maxOffset, scrollOffset + 1);
62
+ else if (matchesKey(data, "pageUp")) scrollOffset = Math.max(0, scrollOffset - pageSize);
63
+ else if (matchesKey(data, "pageDown")) scrollOffset = Math.min(maxOffset, scrollOffset + pageSize);
64
+ else if (matchesKey(data, "home")) scrollOffset = 0;
65
+ else if (matchesKey(data, "end")) scrollOffset = maxOffset;
66
+ tui.requestRender();
67
+ },
68
+ };
69
+ return component;
70
+ });
71
+ }
@@ -0,0 +1,56 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { fileURLToPath } from "node:url";
3
+
4
+ const referenceDirectory = fileURLToPath(new URL("../generated/reference/", import.meta.url));
5
+
6
+ export interface ReferenceEntry {
7
+ id: string;
8
+ path: string;
9
+ title: string;
10
+ summary: string;
11
+ phases: string[];
12
+ }
13
+
14
+ interface ReferenceIndex {
15
+ generated_from: string[];
16
+ references: ReferenceEntry[];
17
+ }
18
+
19
+ let indexPromise: Promise<ReferenceIndex> | undefined;
20
+
21
+ export async function loadReferenceIndex(): Promise<ReferenceIndex> {
22
+ indexPromise ??= readFile(`${referenceDirectory}index.json`, "utf8")
23
+ .then((content) => JSON.parse(content) as ReferenceIndex);
24
+ return indexPromise;
25
+ }
26
+
27
+ export async function relevantReferences(phaseId?: string): Promise<ReferenceEntry[]> {
28
+ const { references } = await loadReferenceIndex();
29
+ if (!phaseId) return references.filter((entry) => entry.phases.includes("*"));
30
+ return references.filter((entry) => entry.phases.includes("*") || entry.phases.includes(phaseId));
31
+ }
32
+
33
+ export async function findReference(topic: string): Promise<ReferenceEntry | undefined> {
34
+ const normalized = normalize(topic);
35
+ const { references } = await loadReferenceIndex();
36
+ return references.find((entry) =>
37
+ normalize(entry.id) === normalized
38
+ || normalize(entry.path) === normalized
39
+ || normalize(entry.title) === normalized
40
+ ) ?? references.find((entry) =>
41
+ normalize(entry.id).includes(normalized)
42
+ || normalize(entry.path).includes(normalized)
43
+ || normalize(entry.title).includes(normalized)
44
+ );
45
+ }
46
+
47
+ export async function readReference(entry: ReferenceEntry): Promise<string> {
48
+ if (entry.path.includes("..") || entry.path.startsWith("/")) {
49
+ throw new Error(`Invalid packaged AHEAD reference path: ${entry.path}`);
50
+ }
51
+ return readFile(`${referenceDirectory}${entry.path}`, "utf8");
52
+ }
53
+
54
+ function normalize(value: string): string {
55
+ return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
56
+ }