@pareto4/pp-core 1.22.0 → 1.23.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 (119) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/bin/install.js +205 -28
  3. package/commands/pp-help.md +2 -1
  4. package/gemini-extension.json +1 -1
  5. package/hooks/dist/pp-check-update-worker.js +1 -1
  6. package/hooks/dist/pp-context-monitor.js +3 -3
  7. package/hooks/dist/pp-statusline.js +88 -14
  8. package/hooks/dist/pp-update-banner.js +44 -4
  9. package/hooks/dist/pp-workflow-guard.js +3 -3
  10. package/hooks/pp-check-update-worker.js +1 -1
  11. package/hooks/pp-context-monitor.js +3 -3
  12. package/hooks/pp-statusline.js +88 -14
  13. package/hooks/pp-update-banner.js +44 -4
  14. package/hooks/pp-workflow-guard.js +3 -3
  15. package/package.json +1 -1
  16. package/pp-core/bin/lib/audience-command.cjs +271 -0
  17. package/pp-core/bin/lib/audience.cjs +293 -0
  18. package/pp-core/bin/lib/commands.cjs +26 -17
  19. package/pp-core/bin/lib/discovery-charter.cjs +13 -1
  20. package/pp-core/bin/lib/discovery-input-check.cjs +11 -11
  21. package/pp-core/bin/lib/discovery-session-check.cjs +7 -7
  22. package/pp-core/bin/lib/discovery-status.cjs +419 -0
  23. package/pp-core/bin/lib/explain-command.cjs +48 -0
  24. package/pp-core/bin/lib/gate-card.cjs +97 -0
  25. package/pp-core/bin/lib/hook-messages.cjs +132 -0
  26. package/pp-core/bin/lib/init-contract.cjs +116 -76
  27. package/pp-core/bin/lib/init-interactive.cjs +110 -49
  28. package/pp-core/bin/lib/init.cjs +44 -3
  29. package/pp-core/bin/lib/io.cjs +33 -0
  30. package/pp-core/bin/lib/message-labels.cjs +75 -0
  31. package/pp-core/bin/lib/messages/core.cjs +20 -0
  32. package/pp-core/bin/lib/messages/discovery.cjs +267 -0
  33. package/pp-core/bin/lib/messages/gate.cjs +257 -0
  34. package/pp-core/bin/lib/messages/hook.cjs +114 -0
  35. package/pp-core/bin/lib/messages/init.cjs +513 -0
  36. package/pp-core/bin/lib/messages/progress.cjs +110 -0
  37. package/pp-core/bin/lib/messages.cjs +208 -0
  38. package/pp-core/bin/lib/vault-command-router.cjs +16 -3
  39. package/pp-core/bin/lib/violation-render.cjs +78 -0
  40. package/pp-core/bin/pp-tools.cjs +68 -23
  41. package/pp-core/bin/shared/config-schema.manifest.json +1 -0
  42. package/pp-core/references/audience-moments.md +119 -0
  43. package/pp-core/references/audience.md +114 -0
  44. package/pp-core/references/discovery-charter.md +2 -2
  45. package/pp-core/templates/project-charter.md +3 -4
  46. package/pp-core/workflows/add-phase.md +2 -0
  47. package/pp-core/workflows/add-tests.md +2 -0
  48. package/pp-core/workflows/add-todo.md +2 -0
  49. package/pp-core/workflows/ai-integration-phase.md +2 -0
  50. package/pp-core/workflows/audit-milestone.md +2 -0
  51. package/pp-core/workflows/autonomous.md +2 -0
  52. package/pp-core/workflows/check-todos.md +2 -0
  53. package/pp-core/workflows/code-review-fix.md +2 -0
  54. package/pp-core/workflows/code-review.md +2 -0
  55. package/pp-core/workflows/complete-milestone.md +10 -1
  56. package/pp-core/workflows/discovery-sprint/charter.md +3 -2
  57. package/pp-core/workflows/discovery-sprint/prefill.md +3 -0
  58. package/pp-core/workflows/discovery-sprint/prototype.md +14 -0
  59. package/pp-core/workflows/discovery-sprint/sessions/01-kick-off.md +4 -1
  60. package/pp-core/workflows/discovery-sprint/sessions/13-wrap-up.md +1 -1
  61. package/pp-core/workflows/discovery-sprint/vault-context.md +15 -0
  62. package/pp-core/workflows/discovery-sprint.md +14 -0
  63. package/pp-core/workflows/discovery-to-build.md +8 -2
  64. package/pp-core/workflows/discuss-phase-assumptions.md +2 -0
  65. package/pp-core/workflows/discuss-phase.md +1 -1
  66. package/pp-core/workflows/edit-phase.md +2 -0
  67. package/pp-core/workflows/eval-review.md +2 -0
  68. package/pp-core/workflows/execute-phase/steps/check-query-gate.md +25 -1
  69. package/pp-core/workflows/execute-phase/steps/vault-fetch.md +1 -0
  70. package/pp-core/workflows/execute-phase/steps/vault-load-gate.md +2 -0
  71. package/pp-core/workflows/execute-phase.md +1 -1
  72. package/pp-core/workflows/execute-plan.md +2 -0
  73. package/pp-core/workflows/extract-learnings.md +2 -0
  74. package/pp-core/workflows/help/modes/business.md +56 -0
  75. package/pp-core/workflows/help.md +12 -2
  76. package/pp-core/workflows/import.md +10 -1
  77. package/pp-core/workflows/ingest-docs.md +2 -0
  78. package/pp-core/workflows/init.md +17 -1
  79. package/pp-core/workflows/insert-phase.md +2 -0
  80. package/pp-core/workflows/list-workspaces.md +2 -0
  81. package/pp-core/workflows/manager.md +2 -0
  82. package/pp-core/workflows/map-codebase.md +2 -0
  83. package/pp-core/workflows/milestone-summary.md +2 -0
  84. package/pp-core/workflows/new-milestone.md +10 -1
  85. package/pp-core/workflows/new-project.md +12 -2
  86. package/pp-core/workflows/new-workspace.md +2 -0
  87. package/pp-core/workflows/next.md +8 -0
  88. package/pp-core/workflows/plan-milestone-gaps.md +10 -1
  89. package/pp-core/workflows/plan-phase.md +2 -1
  90. package/pp-core/workflows/plan-review-convergence.md +1 -1
  91. package/pp-core/workflows/progress.md +10 -0
  92. package/pp-core/workflows/quick.md +11 -1
  93. package/pp-core/workflows/remove-phase.md +2 -0
  94. package/pp-core/workflows/remove-workspace.md +2 -0
  95. package/pp-core/workflows/resume-project.md +25 -0
  96. package/pp-core/workflows/review.md +2 -0
  97. package/pp-core/workflows/scan.md +2 -0
  98. package/pp-core/workflows/secure-phase.md +2 -0
  99. package/pp-core/workflows/settings.md +41 -0
  100. package/pp-core/workflows/ship.md +2 -0
  101. package/pp-core/workflows/spec-phase.md +1 -1
  102. package/pp-core/workflows/ui-phase.md +2 -0
  103. package/pp-core/workflows/ui-review.md +2 -0
  104. package/pp-core/workflows/ultraplan-phase.md +2 -0
  105. package/pp-core/workflows/validate-phase.md +2 -0
  106. package/pp-core/workflows/vault-review.md +2 -0
  107. package/pp-core/workflows/verify-phase.md +2 -0
  108. package/pp-core/workflows/verify-work.md +2 -0
  109. package/scripts/affected-tests-lib.cjs +26 -12
  110. package/scripts/ci-release-only.cjs +158 -0
  111. package/scripts/live-runs/driver.cjs +326 -0
  112. package/scripts/live-runs/run.cjs +906 -0
  113. package/scripts/live-runs/scenarios.cjs +326 -0
  114. package/scripts/live-runs/scratch-project.cjs +132 -0
  115. package/scripts/live-runs/scrub.cjs +291 -0
  116. package/scripts/live-runs/turn-guard.cjs +107 -0
  117. package/scripts/live-runs/walk-setup.cjs +198 -0
  118. package/scripts/run-tests.cjs +151 -135
  119. package/scripts/test-env.cjs +64 -0
@@ -5,7 +5,7 @@
5
5
  * The Project Charter is the one document every stakeholder signs at the end
6
6
  * of a Discovery Sprint. Session 13 drafts it from the session record into
7
7
  * `docs/discovery/PROJECT-CHARTER.md` (client-visible, like `docs/architecture/`),
8
- * in the sprint language, from the fixed-shape template in
8
+ * in the charter language, from the fixed-shape template in
9
9
  * `pp-core/templates/project-charter.md`. This module checks the structure of
10
10
  * that document — never its judgement:
11
11
  *
@@ -148,6 +148,17 @@ function parseFrontmatter(lines) {
148
148
  }
149
149
  return null;
150
150
  }
151
+ /**
152
+ * The charter's frontmatter `status` exactly as `charter-check` reads it: one of
153
+ * `STATUS_VALUES`, else null (no frontmatter, no field, or a value the check
154
+ * reports as `bad_status`). `discovery status` predicts the gate with this, so
155
+ * the two never disagree on what the status is (42-REVIEW WR-01).
156
+ */
157
+ function charterStatusOf(content) {
158
+ const fm = parseFrontmatter(content.split(/\r?\n/));
159
+ const value = fm?.fields.get('status') ?? '';
160
+ return STATUS_VALUES.includes(value) ? value : null;
161
+ }
151
162
  /**
152
163
  * Every pipe table in a run of lines: the header is the row before the first
153
164
  * separator, data rows follow until the first non-pipe line. A second table
@@ -658,6 +669,7 @@ module.exports = {
658
669
  CHARTER_CHECK_FAILED: ERROR_REASON.DISCOVERY_CHARTER_CHECK_FAILED,
659
670
  CHARTER_UNSIGNED: ERROR_REASON.DISCOVERY_CHARTER_UNSIGNED,
660
671
  parseTables,
672
+ charterStatusOf,
661
673
  checkCharter,
662
674
  checkProjectCharter,
663
675
  cmdDiscoveryCharterCheck,
@@ -30,9 +30,10 @@ const node_path_1 = __importDefault(require("node:path"));
30
30
  const node_child_process_1 = require("node:child_process");
31
31
  // eslint-disable-next-line @typescript-eslint/no-require-imports
32
32
  const io = require("./io.cjs");
33
- const { output, error, ERROR_REASON } = io;
33
+ const { output, error, errorCode, ERROR_REASON } = io;
34
34
  // eslint-disable-next-line @typescript-eslint/no-require-imports
35
35
  const fm = require("./frontmatter.cjs");
36
+ const violation_render_cjs_1 = require("./violation-render.cjs");
36
37
  // ─── Constants ────────────────────────────────────────────────────────────────
37
38
  const SESSION_STATUS = Object.freeze({
38
39
  NOT_STARTED: 'Not Started',
@@ -169,14 +170,6 @@ function checkInputPackage(cwd) {
169
170
  return { ok: true, ...summary };
170
171
  }
171
172
  // ─── CLI adapter ──────────────────────────────────────────────────────────────
172
- /** One content-free line per violation: path + structured reason + counts (T-02-05). */
173
- function formatViolation(v) {
174
- if (v.reason === 'confidential_tracked') {
175
- return ` ${v.file}: confidential_tracked`;
176
- }
177
- const score = v.reason === 'score_counts_unconfirmed' ? `score: ${v.score}, ` : '';
178
- return ` ${v.session}: ${v.reason} (${score}markers: ${v.markers})`;
179
- }
180
173
  /**
181
174
  * CLI adapter for `pp-tools discovery input-check` (dotted form
182
175
  * `pp-tools query discovery.input-check`).
@@ -197,8 +190,15 @@ function cmdDiscoveryInputCheck(cwd, args, raw) {
197
190
  }
198
191
  const result = checkInputPackage(cwd);
199
192
  if (!result.ok) {
200
- const lines = result.violations.map(formatViolation).join('\n');
201
- error(`discovery input-check: ${result.violations.length} violation(s):\n${lines}`, ERROR_REASON.DISCOVERY_INPUT_CHECK_FAILED);
193
+ // Content-free contract (T-02-05): each line names a path, a structured reason and counts, never file contents.
194
+ // The code is the ERROR_REASON value itself (D-03); the lines come from the shared renderer (D-11).
195
+ const violations = result.violations;
196
+ errorCode(ERROR_REASON.DISCOVERY_INPUT_CHECK_FAILED, (audience) => ({
197
+ count: violations.length,
198
+ lines: violations
199
+ .map((v) => (0, violation_render_cjs_1.renderViolation)('discovery.input', v, { audience, extra: { marker: TO_CONFIRM_MARKER, status_word: SESSION_STATUS.COMPLETED } }))
200
+ .join('\n'),
201
+ }), ERROR_REASON.DISCOVERY_INPUT_CHECK_FAILED);
202
202
  return; // unreachable — error() exits
203
203
  }
204
204
  output({
@@ -32,9 +32,10 @@ const node_fs_1 = __importDefault(require("node:fs"));
32
32
  const node_path_1 = __importDefault(require("node:path"));
33
33
  // eslint-disable-next-line @typescript-eslint/no-require-imports
34
34
  const io = require("./io.cjs");
35
- const { output, error, ERROR_REASON } = io;
35
+ const { output, error, errorCode, ERROR_REASON } = io;
36
36
  // eslint-disable-next-line @typescript-eslint/no-require-imports
37
37
  const table = require("./discovery-table.cjs");
38
+ const violation_render_cjs_1 = require("./violation-render.cjs");
38
39
  // ─── Constants ────────────────────────────────────────────────────────────────
39
40
  const DISCOVERY_DIR = '.planning/discovery';
40
41
  const SESSION_COUNT = 13;
@@ -213,11 +214,6 @@ function checkProjectSessions(cwd, numbers, sessionsDir = SESSIONS_DIR) {
213
214
  const sessions = numbers.map((n) => checkProjectSession(cwd, n, sessionsDir));
214
215
  return { ok: sessions.every((s) => s.violations.length === 0), sessions };
215
216
  }
216
- function formatViolation(v) {
217
- const where = v.line !== undefined ? `:${v.line}` : '';
218
- const what = v.id !== undefined ? ` (${v.id})` : '';
219
- return ` - ${v.artifact}${where}: ${v.reason}${what}`;
220
- }
221
217
  // ─── CLI adapter ──────────────────────────────────────────────────────────────
222
218
  /**
223
219
  * `pp-tools discovery session-check <N> [<N>…] | --all` (dotted
@@ -245,7 +241,11 @@ function cmdDiscoverySessionCheck(cwd, args, raw) {
245
241
  if (!result.ok) {
246
242
  const violations = result.sessions.flatMap((s) => s.violations);
247
243
  output({ ...payload, violations: violations.length }, raw);
248
- error(`discovery session-check: ${violations.length} violation(s):\n` + violations.map(formatViolation).join('\n'), ERROR_REASON.DISCOVERY_SESSION_CHECK_FAILED);
244
+ // The code is the ERROR_REASON value itself (D-03); the lines come from the shared renderer (D-11).
245
+ errorCode(ERROR_REASON.DISCOVERY_SESSION_CHECK_FAILED, (audience) => ({
246
+ count: violations.length,
247
+ lines: violations.map((v) => (0, violation_render_cjs_1.renderViolation)('discovery.session', v, { audience, extra: { none: NONE_LITERAL } })).join('\n'),
248
+ }), ERROR_REASON.DISCOVERY_SESSION_CHECK_FAILED);
249
249
  return; // unreachable
250
250
  }
251
251
  output(payload, raw);
@@ -0,0 +1,419 @@
1
+ "use strict";
2
+ /**
3
+ * `pp-tools discovery status` (Phase 42, DOOR-01; D-01..D-08).
4
+ *
5
+ * ADR-457 build-at-publish: this TypeScript file is the SOURCE OF TRUTH. It
6
+ * compiles to the gitignored `pp-core/bin/lib/discovery-status.cjs`; do NOT
7
+ * hand-edit the compiled output.
8
+ *
9
+ * Tells a project that is only in the Discovery Sprint from one that has a build,
10
+ * and shows the sprint's state in the person's own wording through the Phase 41
11
+ * catalog: a headline count, one row per session, where the sprint stopped and
12
+ * the next step with its command. The wording lives in `src/messages/discovery.cts`
13
+ * (`discovery.status*`); this module supplies every slot.
14
+ *
15
+ * Display only: the JSON keeps the canonical status words and the canonical
16
+ * commands, and a status word is relabelled for the person only at the last step,
17
+ * when a row or a line is written (D-13). `--raw` prints the same JSON for every
18
+ * audience (41 D-08); only the text view follows the audience.
19
+ *
20
+ * Everything echoed from the tracker or the charter (project name, session names,
21
+ * charter status) is cleaned for EVERY audience, developer included, because the
22
+ * developer path of the catalog does not sanitise (T-42-01).
23
+ */
24
+ var __importDefault = (this && this.__importDefault) || function (mod) {
25
+ return (mod && mod.__esModule) ? mod : { "default": mod };
26
+ };
27
+ Object.defineProperty(exports, "__esModule", { value: true });
28
+ exports.MAX_FILE_BYTES = exports.REGISTRY_SIZE = exports.MAX_ROWS = exports.TRACKER_FILE = void 0;
29
+ exports.renderStatus = renderStatus;
30
+ exports.computeStatus = computeStatus;
31
+ exports.cmdDiscoveryStatus = cmdDiscoveryStatus;
32
+ const node_fs_1 = __importDefault(require("node:fs"));
33
+ const node_path_1 = __importDefault(require("node:path"));
34
+ const messages_cjs_1 = require("./messages.cjs");
35
+ const message_labels_cjs_1 = require("./message-labels.cjs");
36
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
37
+ const io = require("./io.cjs");
38
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
39
+ const table = require("./discovery-table.cjs");
40
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
41
+ const inputCheck = require("./discovery-input-check.cjs");
42
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
43
+ const charterMod = require("./discovery-charter.cjs");
44
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
45
+ const planningWorkspace = require("./planning-workspace.cjs");
46
+ const { output, ERROR_REASON } = io;
47
+ const { SESSION_STATUS } = inputCheck;
48
+ const { CHARTER_FILE, SIGNED_VALUES } = charterMod;
49
+ /** Explicitly typed so TypeScript narrows after a failing guard (the destructured `io.error` does not). */
50
+ function error(message, reason) {
51
+ return io.error(message, reason);
52
+ }
53
+ /** The progress sheet every discovery session start and prefill creates (D-01). */
54
+ exports.TRACKER_FILE = '.planning/discovery/_progress.md';
55
+ /** The tracker's table is examined up to the table-primitive span, never beyond (T-42-03). */
56
+ exports.MAX_ROWS = 99;
57
+ /**
58
+ * The Session Registry's size (discovery-sprint.md). Only sessions 1..13 count, a
59
+ * registry session with no row counts as Not Started, and the totals are always
60
+ * out of 13, as Flow 1 counts them (42-REVIEW WR-02).
61
+ */
62
+ exports.REGISTRY_SIZE = 13;
63
+ /** A tracker or charter larger than this is never read (T-42-03). */
64
+ exports.MAX_FILE_BYTES = 1048576;
65
+ /** The columns a Session Progress table must have, by lower-cased header name (D-06). */
66
+ const REQUIRED_COLUMNS = ['#', 'session', 'status'];
67
+ const PLACEHOLDER_PROJECT = '[PROJECT_NAME]';
68
+ const ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
69
+ const SESSION_NUMBER_RE = /^\d{1,2}$/;
70
+ /** Every Unicode format character (bidi overrides and isolates, zero-width, BOM, tag characters). */
71
+ const FORMAT_CHARS_RE = /\p{Cf}/gu;
72
+ /**
73
+ * A value read from a file, made safe to print for every audience: format
74
+ * characters removed first, then control characters stripped, whitespace
75
+ * collapsed and the length capped (T-42-01).
76
+ */
77
+ function cleanText(v) {
78
+ if (typeof v !== 'string' && typeof v !== 'number')
79
+ return '';
80
+ return (0, messages_cjs_1.sanitizeParam)(String(v).replace(FORMAT_CHARS_RE, ''));
81
+ }
82
+ /** A table cell: cleaned, and a `|` can never add a column (T-42-02). */
83
+ function cell(v) {
84
+ return cleanText(v).replace(/\|/g, '/');
85
+ }
86
+ /**
87
+ * The sprint view for one audience. Text only: every visible word comes from the
88
+ * catalog, every status word through `label()`, and business and engineer text
89
+ * ends with exactly one `Ref:` line (developer text has none).
90
+ */
91
+ function renderStatus(status, audience) {
92
+ if (!status.in_discovery) {
93
+ return (0, messages_cjs_1.msg)('discovery.status.none', { file: exports.TRACKER_FILE, command: '/pp-discovery 1' }, { audience }) + '\n';
94
+ }
95
+ if (status.tracker === 'unreadable') {
96
+ return (0, messages_cjs_1.msg)('discovery.status.unreadable', { file: exports.TRACKER_FILE, command: '/pp-discovery' }, { audience }) + '\n';
97
+ }
98
+ const words = (code) => (0, messages_cjs_1.text)(code, {}, { audience });
99
+ const nameOf = (name) => cell(name) || words('discovery.status.session_unnamed');
100
+ const wording = (canonical) => (0, message_labels_cjs_1.label)('discovery.status', String(canonical), { audience });
101
+ const counts = status.counts ?? { total: 0, completed: 0, in_progress: 0, pre_filled: 0, not_started: 0 };
102
+ const sessions = status.sessions ?? [];
103
+ const out = [];
104
+ out.push((0, messages_cjs_1.text)('discovery.status', { project: cleanText(status.project) || words('discovery.status.project_unknown') }, { audience }));
105
+ out.push('');
106
+ out.push((0, messages_cjs_1.text)('discovery.status.counts', {
107
+ done: counts.completed,
108
+ total: counts.total,
109
+ in_progress: counts.in_progress,
110
+ pre_filled: counts.pre_filled,
111
+ not_started: counts.not_started,
112
+ }, { audience }));
113
+ out.push('');
114
+ out.push(words('discovery.status.header'));
115
+ for (const s of sessions) {
116
+ out.push(`| ${String(s.number)} | ${nameOf(s.name)} | ${wording(s.status)} |`);
117
+ }
118
+ out.push('');
119
+ const unknown = status.unknown_status ?? [];
120
+ if (unknown.length > 0) {
121
+ out.push((0, messages_cjs_1.text)('discovery.status.unknown_status', { sessions: unknown.join(', ') }, { audience }));
122
+ }
123
+ const stopped = status.stopped_at;
124
+ if (stopped !== undefined && stopped.basis === 'in_progress') {
125
+ out.push((0, messages_cjs_1.text)('discovery.status.stopped.in_progress', { session: stopped.session ?? '', name: nameOf(stopped.name), status: wording(stopped.status ?? '') }, { audience }));
126
+ }
127
+ else if (stopped !== undefined && stopped.basis === 'completed') {
128
+ out.push((0, messages_cjs_1.text)('discovery.status.stopped.completed', { session: stopped.session ?? '', name: nameOf(stopped.name) }, { audience }));
129
+ }
130
+ else {
131
+ out.push(words('discovery.status.stopped.none'));
132
+ }
133
+ const next = status.next;
134
+ if (next !== undefined) {
135
+ const named = (code) => {
136
+ const target = sessions.find((s) => s.number === next.session);
137
+ return (0, messages_cjs_1.text)(code, { session: next.session ?? '', name: nameOf(target === undefined ? '' : target.name), command: next.command }, { audience });
138
+ };
139
+ switch (next.kind) {
140
+ case 'resume':
141
+ out.push(named('discovery.status.next.resume'));
142
+ break;
143
+ case 'confirm':
144
+ out.push(named('discovery.status.next.confirm'));
145
+ break;
146
+ case 'start':
147
+ out.push(named('discovery.status.next.start'));
148
+ break;
149
+ case 'charter_draft':
150
+ out.push((0, messages_cjs_1.text)('discovery.status.next.charter_draft', { command: next.command }, { audience }));
151
+ break;
152
+ case 'charter_sign':
153
+ out.push((0, messages_cjs_1.text)('discovery.status.next.charter_sign', {
154
+ command: next.command,
155
+ charter_status: cleanText(status.charter?.status) || words('discovery.status.charter_no_status'),
156
+ }, { audience }));
157
+ break;
158
+ case 'to_build':
159
+ out.push((0, messages_cjs_1.text)('discovery.status.next.to_build', { command: next.command }, { audience }));
160
+ break;
161
+ case 'open_sprint':
162
+ // With a readable tracker: a session with an unrecognised status is the lowest open one (WR-03).
163
+ out.push((0, messages_cjs_1.text)('discovery.status.next.check_sheet', { command: next.command }, { audience }));
164
+ break;
165
+ }
166
+ }
167
+ if (audience !== 'developer')
168
+ out.push((0, messages_cjs_1.refLine)('discovery.status'));
169
+ return out.join('\n') + '\n';
170
+ }
171
+ // ─── computeStatus ────────────────────────────────────────────────────────────
172
+ const CANONICAL_WORDS = [
173
+ SESSION_STATUS.NOT_STARTED,
174
+ SESSION_STATUS.PRE_FILLED,
175
+ SESSION_STATUS.IN_PROGRESS,
176
+ SESSION_STATUS.COMPLETED,
177
+ ];
178
+ /**
179
+ * What is at `p`: a regular file within the size bound, something else, or nothing.
180
+ * The tracker is judged without following a link; the charter follows one, as
181
+ * `charter-check` does (42-REVIEW WR-01).
182
+ */
183
+ function kindOf(p, followLink = false) {
184
+ try {
185
+ const st = followLink ? node_fs_1.default.statSync(p) : node_fs_1.default.lstatSync(p);
186
+ return st.isFile() && st.size <= exports.MAX_FILE_BYTES ? 'regular' : 'other';
187
+ }
188
+ catch {
189
+ return 'absent';
190
+ }
191
+ }
192
+ function readText(p) {
193
+ try {
194
+ return node_fs_1.default.readFileSync(p, 'utf8');
195
+ }
196
+ catch {
197
+ return null;
198
+ }
199
+ }
200
+ /** The sessions of a tracker's `## Session Progress` table, or null when the table cannot be read (D-06, D-08). */
201
+ function parseSessions(content) {
202
+ const lines = content.split(/\r?\n/);
203
+ const section = table.splitSections(lines, 2).find((s) => s.heading.trim().toLowerCase() === 'session progress');
204
+ if (section === undefined)
205
+ return null;
206
+ const parsed = table.parseTables(section.body)[0];
207
+ if (parsed === undefined)
208
+ return null;
209
+ const header = parsed.header.map((h) => h.trim().toLowerCase());
210
+ const [iNumber, iName, iStatus] = REQUIRED_COLUMNS.map((c) => header.indexOf(c));
211
+ if (iNumber < 0 || iName < 0 || iStatus < 0)
212
+ return null;
213
+ const iDone = header.indexOf('completed');
214
+ const byNumber = new Map();
215
+ const unknown = [];
216
+ for (const row of parsed.rows.slice(0, exports.MAX_ROWS)) {
217
+ const numberCell = (row.cells[iNumber] ?? '').trim();
218
+ if (!SESSION_NUMBER_RE.test(numberCell))
219
+ continue;
220
+ const number = Number(numberCell);
221
+ // Outside the registry: `/pp-discovery` rejects it, so it is never a session here (WR-02).
222
+ if (number < 1 || number > exports.REGISTRY_SIZE || byNumber.has(number))
223
+ continue;
224
+ const wordCell = (row.cells[iStatus] ?? '').trim().replace(/\s+/g, ' ').toLowerCase();
225
+ const canonical = CANONICAL_WORDS.find((w) => w.toLowerCase() === wordCell);
226
+ if (canonical === undefined)
227
+ unknown.push(number);
228
+ const doneCell = iDone >= 0 ? (row.cells[iDone] ?? '').trim() : '';
229
+ byNumber.set(number, {
230
+ number,
231
+ name: cleanText(row.cells[iName] ?? ''),
232
+ status: canonical ?? SESSION_STATUS.NOT_STARTED,
233
+ completed: ISO_DATE_RE.test(doneCell) ? doneCell : null,
234
+ });
235
+ }
236
+ if (byNumber.size === 0)
237
+ return null;
238
+ // A registry session with no row has not been held: Not Started, so a truncated
239
+ // tracker is never "every session done" (WR-02).
240
+ for (let n = 1; n <= exports.REGISTRY_SIZE; n += 1) {
241
+ if (!byNumber.has(n))
242
+ byNumber.set(n, { number: n, name: '', status: SESSION_STATUS.NOT_STARTED, completed: null });
243
+ }
244
+ const sessions = [...byNumber.values()].sort((a, b) => a.number - b.number);
245
+ return { sessions, unknown: unknown.sort((a, b) => a - b) };
246
+ }
247
+ /**
248
+ * The Project Charter's state, read only when every session is Completed (D-05, D-06).
249
+ * The status is read by `charter-check`'s own reader, so "signed" here means what
250
+ * `/pp-discovery-to-build`'s gate means by it (42-REVIEW WR-01).
251
+ */
252
+ function charterOf(cwd) {
253
+ const file = node_path_1.default.join(cwd, CHARTER_FILE);
254
+ const kind = kindOf(file, true);
255
+ if (kind === 'absent')
256
+ return { state: 'missing', status: null };
257
+ let value = null;
258
+ if (kind === 'regular') {
259
+ const content = readText(file);
260
+ if (content !== null)
261
+ value = charterMod.charterStatusOf(content);
262
+ }
263
+ const signed = value !== null && SIGNED_VALUES.includes(value);
264
+ return { state: signed ? 'signed' : 'unsigned', status: value === null ? null : cleanText(value) || null };
265
+ }
266
+ /**
267
+ * Flow 1's rule (discovery-sprint.md, Flow 1 step 3): in progress, then pre-filled, then not started, then the charter.
268
+ * A session whose status word is not recognised counts as not started but is never
269
+ * the session to start: it may be a finished workshop with a typo (`Done`). When it
270
+ * is the lowest open session the next step is the sprint itself, so the sheet is
271
+ * checked first (42-REVIEW WR-03).
272
+ */
273
+ function nextOf(sessions, unknown, charter) {
274
+ const unknownSet = new Set(unknown);
275
+ const lowest = (word) => sessions.find((s) => s.status === word && !unknownSet.has(s.number));
276
+ const firstUnknown = sessions.find((s) => unknownSet.has(s.number));
277
+ const open = [
278
+ [SESSION_STATUS.IN_PROGRESS, 'resume'],
279
+ [SESSION_STATUS.PRE_FILLED, 'confirm'],
280
+ [SESSION_STATUS.NOT_STARTED, 'start'],
281
+ ];
282
+ for (const [word, kind] of open) {
283
+ const hit = lowest(word);
284
+ if (kind === 'start' && firstUnknown !== undefined && (hit === undefined || firstUnknown.number < hit.number)) {
285
+ return { kind: 'open_sprint', session: null, command: '/pp-discovery' };
286
+ }
287
+ // The command is built from the integer session number, never from file text (T-42-05).
288
+ if (hit !== undefined)
289
+ return { kind, session: hit.number, command: `/pp-discovery ${String(hit.number)}` };
290
+ }
291
+ if (charter.state === 'missing')
292
+ return { kind: 'charter_draft', session: null, command: '/pp-discovery 13' };
293
+ if (charter.state === 'signed')
294
+ return { kind: 'to_build', session: null, command: '/pp-discovery-to-build' };
295
+ return { kind: 'charter_sign', session: null, command: '/pp-discovery charter' };
296
+ }
297
+ /** Where the sprint stopped (D-08): the lowest In Progress session, else the one completed last, else nowhere. */
298
+ function stoppedOf(sessions) {
299
+ const inProgress = sessions.find((s) => s.status === SESSION_STATUS.IN_PROGRESS);
300
+ if (inProgress !== undefined) {
301
+ return { basis: 'in_progress', session: inProgress.number, name: inProgress.name, status: SESSION_STATUS.IN_PROGRESS };
302
+ }
303
+ const done = sessions.filter((s) => s.status === SESSION_STATUS.COMPLETED);
304
+ if (done.length === 0)
305
+ return { basis: 'none', session: null, name: null, status: null };
306
+ let best = done[0];
307
+ for (const s of done) {
308
+ // Sessions are sorted by number, so `>=` makes a tie, or a blank date, go to the higher number.
309
+ if ((s.completed ?? '') >= (best.completed ?? ''))
310
+ best = s;
311
+ }
312
+ return { basis: 'completed', session: best.number, name: best.name, status: SESSION_STATUS.COMPLETED };
313
+ }
314
+ /**
315
+ * The sprint's state for `cwd`. Read-only and never throws: every file step has
316
+ * its own guard, and a workspace setting that cannot be resolved answers "not
317
+ * discovery-only", so a broken workstream setup keeps today's routing (research A4).
318
+ */
319
+ function computeStatus(cwd) {
320
+ let projectExists = false;
321
+ let roadmapExists = false;
322
+ let workspaceError = false;
323
+ try {
324
+ // The resolvers init.progress and init.resume use (D-02). STATE.md is never read.
325
+ projectExists = planningWorkspace.resolveProjectFile(cwd).exists;
326
+ roadmapExists = node_fs_1.default.existsSync(node_path_1.default.join(planningWorkspace.planningDir(cwd), 'ROADMAP.md'));
327
+ }
328
+ catch {
329
+ projectExists = false;
330
+ roadmapExists = false;
331
+ workspaceError = true;
332
+ }
333
+ const hasBuild = projectExists || roadmapExists;
334
+ let trackerPath = '';
335
+ let trackerKind = 'absent';
336
+ try {
337
+ trackerPath = node_path_1.default.join(planningWorkspace.planningRoot(cwd), 'discovery', '_progress.md');
338
+ trackerKind = kindOf(trackerPath);
339
+ }
340
+ catch {
341
+ trackerKind = 'absent';
342
+ }
343
+ const facts = {
344
+ has_build: hasBuild,
345
+ project_exists: projectExists,
346
+ roadmap_exists: roadmapExists,
347
+ workspace_error: workspaceError,
348
+ };
349
+ if (trackerKind === 'absent') {
350
+ return { discovery_only: false, in_discovery: false, ...facts, tracker: 'absent' };
351
+ }
352
+ const discoveryOnly = !hasBuild && !workspaceError;
353
+ const unreadable = (project) => ({
354
+ discovery_only: discoveryOnly,
355
+ in_discovery: true,
356
+ ...facts,
357
+ tracker: 'unreadable',
358
+ project,
359
+ next: { kind: 'open_sprint', session: null, command: '/pp-discovery' },
360
+ });
361
+ try {
362
+ const content = trackerKind === 'regular' ? readText(trackerPath) : null;
363
+ if (content === null)
364
+ return unreadable(null);
365
+ let project = null;
366
+ for (const line of content.split(/\r?\n/)) {
367
+ const m = /^\*\*Project:\*\*\s*(.+)$/.exec(line);
368
+ if (m !== null) {
369
+ const name = cleanText(m[1]);
370
+ project = name === '' || name === PLACEHOLDER_PROJECT ? null : name;
371
+ break;
372
+ }
373
+ }
374
+ const parsed = parseSessions(content);
375
+ if (parsed === null)
376
+ return unreadable(project);
377
+ const { sessions, unknown } = parsed;
378
+ const count = (word) => sessions.filter((s) => s.status === word).length;
379
+ const counts = {
380
+ total: sessions.length,
381
+ completed: count(SESSION_STATUS.COMPLETED),
382
+ in_progress: count(SESSION_STATUS.IN_PROGRESS),
383
+ pre_filled: count(SESSION_STATUS.PRE_FILLED),
384
+ not_started: count(SESSION_STATUS.NOT_STARTED),
385
+ };
386
+ // The charter matters only once all 13 sessions are Completed; until then it is not read.
387
+ const charter = counts.completed === exports.REGISTRY_SIZE ? charterOf(cwd) : { state: 'not_checked', status: null };
388
+ return {
389
+ discovery_only: discoveryOnly,
390
+ in_discovery: true,
391
+ ...facts,
392
+ tracker: 'ok',
393
+ project,
394
+ counts,
395
+ sessions,
396
+ unknown_status: unknown,
397
+ stopped_at: stoppedOf(sessions),
398
+ charter,
399
+ next: nextOf(sessions, unknown, charter),
400
+ };
401
+ }
402
+ catch {
403
+ return unreadable(null);
404
+ }
405
+ }
406
+ /**
407
+ * `pp-tools discovery status [--raw]`. `--raw` prints the JSON, the same for every
408
+ * audience (41 D-08); without it the view is printed in the person's wording.
409
+ * Do not copy `cmdProgressRender`, whose `--raw` prints audience text (41-REVIEW WR-05).
410
+ */
411
+ function cmdDiscoveryStatus(cwd, args, raw) {
412
+ if (args.slice(2).length > 0)
413
+ error('Usage: pp-tools discovery status [--raw]', ERROR_REASON.USAGE);
414
+ const status = computeStatus(cwd);
415
+ if (raw)
416
+ output(status, false);
417
+ else
418
+ output(status, true, renderStatus(status, (0, messages_cjs_1.audienceOf)(process.env)));
419
+ }
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ /**
3
+ * `pp-tools explain <code>`: the longer explanation of a message's `Ref:` code
4
+ * (Phase 41: D-19, MSG-05).
5
+ *
6
+ * ADR-457 build-at-publish: this TypeScript file is the SOURCE OF TRUTH. It
7
+ * compiles to the gitignored `pp-core/bin/lib/explain-command.cjs`; do NOT
8
+ * hand-edit the compiled output.
9
+ *
10
+ * A thin adapter over the message catalog. The code is only ever looked up in
11
+ * the catalog's Map (`explainText`), never used to index an object, so
12
+ * `__proto__`, `constructor` and `toString` are unknown codes like any other
13
+ * (T-41-13). The typed code is echoed only after `sanitizeParam` has stripped
14
+ * control characters (T-41-11). There is no listing mode: a bare call prints
15
+ * only the usage (T-41-25).
16
+ */
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.USAGE = void 0;
19
+ exports.cmdExplain = cmdExplain;
20
+ const messages_cjs_1 = require("./messages.cjs");
21
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
22
+ const io = require("./io.cjs");
23
+ const { output, ERROR_REASON } = io;
24
+ /** Explicitly typed so TypeScript narrows after a failing guard (the destructured `io.error` does not). */
25
+ function error(message, reason) {
26
+ return io.error(message, reason);
27
+ }
28
+ const HELP_FLAGS = ['-h', '--help', '-?', '--h', '--usage'];
29
+ exports.USAGE = 'Usage: pp-tools explain <code> [--raw] — <code> is the text after "Ref:" at the end of a message';
30
+ function cmdExplain(_cwd, args, raw) {
31
+ if (args.length === 1 && HELP_FLAGS.includes(args[0])) {
32
+ output({ usage: exports.USAGE }, true, exports.USAGE + '\n');
33
+ return;
34
+ }
35
+ if (args.length !== 1 || args[0].startsWith('--'))
36
+ error(exports.USAGE, ERROR_REASON.USAGE);
37
+ const code = args[0];
38
+ const audience = (0, messages_cjs_1.audienceOf)(process.env);
39
+ const body = (0, messages_cjs_1.explainText)(code, { audience });
40
+ if (body === null) {
41
+ io.errorCode('explain_unknown_code', { code: (0, messages_cjs_1.sanitizeParam)(code) }, ERROR_REASON.EXPLAIN_UNKNOWN_CODE);
42
+ }
43
+ if (raw) {
44
+ output({ code, audience, text: body }, false);
45
+ return;
46
+ }
47
+ output({ code, audience, text: body }, true, body + '\n');
48
+ }