@pareto4/pp-core 1.21.1 → 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.
- package/.claude-plugin/plugin.json +1 -1
- package/bin/install.js +205 -28
- package/commands/pp-help.md +2 -1
- package/gemini-extension.json +1 -1
- package/hooks/dist/pp-check-update-worker.js +1 -1
- package/hooks/dist/pp-context-monitor.js +3 -3
- package/hooks/dist/pp-statusline.js +88 -14
- package/hooks/dist/pp-update-banner.js +44 -4
- package/hooks/dist/pp-workflow-guard.js +3 -3
- package/hooks/pp-check-update-worker.js +1 -1
- package/hooks/pp-context-monitor.js +3 -3
- package/hooks/pp-statusline.js +88 -14
- package/hooks/pp-update-banner.js +44 -4
- package/hooks/pp-workflow-guard.js +3 -3
- package/package.json +2 -2
- package/pp-core/bin/lib/audience-command.cjs +271 -0
- package/pp-core/bin/lib/audience.cjs +293 -0
- package/pp-core/bin/lib/commands.cjs +26 -17
- package/pp-core/bin/lib/discovery-charter.cjs +13 -1
- package/pp-core/bin/lib/discovery-flow-page.cjs +191 -0
- package/pp-core/bin/lib/discovery-for-phase.cjs +33 -5
- package/pp-core/bin/lib/discovery-input-check.cjs +11 -11
- package/pp-core/bin/lib/discovery-processes.cjs +97 -4
- package/pp-core/bin/lib/discovery-session-check.cjs +7 -7
- package/pp-core/bin/lib/discovery-status.cjs +419 -0
- package/pp-core/bin/lib/discovery-stories.cjs +36 -5
- package/pp-core/bin/lib/discovery-tests.cjs +398 -0
- package/pp-core/bin/lib/explain-command.cjs +48 -0
- package/pp-core/bin/lib/gate-card.cjs +97 -0
- package/pp-core/bin/lib/hook-messages.cjs +132 -0
- package/pp-core/bin/lib/init-contract.cjs +116 -76
- package/pp-core/bin/lib/init-interactive.cjs +110 -49
- package/pp-core/bin/lib/init.cjs +44 -3
- package/pp-core/bin/lib/io.cjs +38 -0
- package/pp-core/bin/lib/message-labels.cjs +75 -0
- package/pp-core/bin/lib/messages/core.cjs +20 -0
- package/pp-core/bin/lib/messages/discovery.cjs +267 -0
- package/pp-core/bin/lib/messages/gate.cjs +257 -0
- package/pp-core/bin/lib/messages/hook.cjs +114 -0
- package/pp-core/bin/lib/messages/init.cjs +513 -0
- package/pp-core/bin/lib/messages/progress.cjs +110 -0
- package/pp-core/bin/lib/messages.cjs +208 -0
- package/pp-core/bin/lib/vault-command-router.cjs +16 -3
- package/pp-core/bin/lib/violation-render.cjs +78 -0
- package/pp-core/bin/pp-tools.cjs +92 -23
- package/pp-core/bin/shared/config-schema.manifest.json +1 -0
- package/pp-core/references/audience-moments.md +119 -0
- package/pp-core/references/audience.md +114 -0
- package/pp-core/references/discovery-build-consumption.md +5 -2
- package/pp-core/references/discovery-charter.md +2 -2
- package/pp-core/references/discovery-id-conventions.md +1 -1
- package/pp-core/references/discovery-processes.md +31 -7
- package/pp-core/references/discovery-session-output.md +1 -1
- package/pp-core/references/discovery-tests.md +186 -0
- package/pp-core/templates/project-charter.md +3 -4
- package/pp-core/workflows/add-phase.md +2 -0
- package/pp-core/workflows/add-tests.md +2 -0
- package/pp-core/workflows/add-todo.md +2 -0
- package/pp-core/workflows/ai-integration-phase.md +2 -0
- package/pp-core/workflows/audit-milestone.md +2 -0
- package/pp-core/workflows/autonomous.md +2 -0
- package/pp-core/workflows/check-todos.md +2 -0
- package/pp-core/workflows/code-review-fix.md +2 -0
- package/pp-core/workflows/code-review.md +2 -0
- package/pp-core/workflows/complete-milestone.md +10 -1
- package/pp-core/workflows/discovery-sprint/charter.md +3 -2
- package/pp-core/workflows/discovery-sprint/prefill.md +3 -0
- package/pp-core/workflows/discovery-sprint/prototype.md +14 -0
- package/pp-core/workflows/discovery-sprint/session-13-gates.md +2 -0
- package/pp-core/workflows/discovery-sprint/sessions/01-kick-off.md +4 -1
- package/pp-core/workflows/discovery-sprint/sessions/03-to-be.md +135 -76
- package/pp-core/workflows/discovery-sprint/sessions/09-qa.md +94 -64
- package/pp-core/workflows/discovery-sprint/sessions/13-wrap-up.md +3 -2
- package/pp-core/workflows/discovery-sprint/vault-context.md +15 -0
- package/pp-core/workflows/discovery-sprint.md +16 -2
- package/pp-core/workflows/discovery-to-build/steps/handoff-gates.md +16 -9
- package/pp-core/workflows/discovery-to-build/templates/discovery-references.md +1 -1
- package/pp-core/workflows/discovery-to-build/templates/pp-handoff.md +9 -1
- package/pp-core/workflows/discovery-to-build.md +10 -4
- package/pp-core/workflows/discuss-phase/discovery.md +2 -2
- package/pp-core/workflows/discuss-phase-assumptions.md +2 -0
- package/pp-core/workflows/discuss-phase.md +1 -1
- package/pp-core/workflows/edit-phase.md +2 -0
- package/pp-core/workflows/eval-review.md +2 -0
- package/pp-core/workflows/execute-phase/steps/check-query-gate.md +25 -1
- package/pp-core/workflows/execute-phase/steps/vault-fetch.md +1 -0
- package/pp-core/workflows/execute-phase/steps/vault-load-gate.md +2 -0
- package/pp-core/workflows/execute-phase.md +1 -1
- package/pp-core/workflows/execute-plan.md +2 -0
- package/pp-core/workflows/extract-learnings.md +2 -0
- package/pp-core/workflows/help/modes/business.md +56 -0
- package/pp-core/workflows/help.md +12 -2
- package/pp-core/workflows/import.md +10 -1
- package/pp-core/workflows/ingest-docs.md +2 -0
- package/pp-core/workflows/init.md +17 -1
- package/pp-core/workflows/insert-phase.md +2 -0
- package/pp-core/workflows/list-workspaces.md +2 -0
- package/pp-core/workflows/manager.md +2 -0
- package/pp-core/workflows/map-codebase.md +2 -0
- package/pp-core/workflows/milestone-summary.md +2 -0
- package/pp-core/workflows/new-milestone.md +10 -1
- package/pp-core/workflows/new-project.md +12 -2
- package/pp-core/workflows/new-workspace.md +2 -0
- package/pp-core/workflows/next.md +8 -0
- package/pp-core/workflows/plan-milestone-gaps.md +10 -1
- package/pp-core/workflows/plan-phase.md +2 -1
- package/pp-core/workflows/plan-review-convergence.md +1 -1
- package/pp-core/workflows/progress.md +10 -0
- package/pp-core/workflows/quick.md +11 -1
- package/pp-core/workflows/remove-phase.md +2 -0
- package/pp-core/workflows/remove-workspace.md +2 -0
- package/pp-core/workflows/resume-project.md +25 -0
- package/pp-core/workflows/review.md +2 -0
- package/pp-core/workflows/scan.md +2 -0
- package/pp-core/workflows/secure-phase.md +2 -0
- package/pp-core/workflows/settings.md +41 -0
- package/pp-core/workflows/ship.md +2 -0
- package/pp-core/workflows/spec-phase.md +1 -1
- package/pp-core/workflows/ui-phase.md +2 -0
- package/pp-core/workflows/ui-review.md +2 -0
- package/pp-core/workflows/ultraplan-phase.md +2 -0
- package/pp-core/workflows/validate-phase.md +2 -0
- package/pp-core/workflows/vault-review.md +2 -0
- package/pp-core/workflows/verify-phase.md +2 -0
- package/pp-core/workflows/verify-work.md +2 -0
- package/scripts/affected-tests-lib.cjs +26 -12
- package/scripts/ci-release-only.cjs +158 -0
- package/scripts/live-runs/driver.cjs +326 -0
- package/scripts/live-runs/run.cjs +906 -0
- package/scripts/live-runs/scenarios.cjs +326 -0
- package/scripts/live-runs/scratch-project.cjs +132 -0
- package/scripts/live-runs/scrub.cjs +291 -0
- package/scripts/live-runs/turn-guard.cjs +107 -0
- package/scripts/live-runs/walk-setup.cjs +198 -0
- package/scripts/run-tests.cjs +151 -132
- package/scripts/test-env.cjs +64 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* `pp-tools audience status|set|answer|fields`: the CLI adapter over the per-person
|
|
4
|
+
* audience setting (Phase 39: AUD-01, AUD-04; D-12, D-14; `fields` is Phase 40 CR-01).
|
|
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/audience-command.cjs`; do NOT
|
|
8
|
+
* hand-edit the compiled output.
|
|
9
|
+
*
|
|
10
|
+
* The person file is read and written only through `./audience.cjs`. Nothing
|
|
11
|
+
* here touches `.planning/config.json`, `~/.gsd/defaults.json` or any
|
|
12
|
+
* discovery file (D-01, D-13). `status` prints short human lines by default and
|
|
13
|
+
* one JSON object with `--raw`; workflows always call it with `--raw`.
|
|
14
|
+
*/
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.buildAudienceStatus = buildAudienceStatus;
|
|
17
|
+
exports.cmdAudience = cmdAudience;
|
|
18
|
+
const audience_cjs_1 = require("./audience.cjs");
|
|
19
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
20
|
+
const io = require("./io.cjs");
|
|
21
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
22
|
+
const configLoader = require("./config-loader.cjs");
|
|
23
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports -- init.cjs is an export= CommonJS module
|
|
24
|
+
const initMod = require("./init.cjs");
|
|
25
|
+
const { output, ERROR_REASON } = io;
|
|
26
|
+
/** Explicitly typed so TypeScript narrows after a failing guard (the destructured `io.error` does not). */
|
|
27
|
+
function error(message, reason) {
|
|
28
|
+
return io.error(message, reason);
|
|
29
|
+
}
|
|
30
|
+
const AUDIENCE_LIST = audience_cjs_1.AUDIENCES.join(', ');
|
|
31
|
+
const LANGUAGE_LIST = audience_cjs_1.LANGUAGES.join(', ');
|
|
32
|
+
const SET_USAGE = 'Usage: pp-tools audience set <business|engineer|developer> [--language en | --clear-language], ' +
|
|
33
|
+
'or pp-tools audience set --clear-language (clears only the language)';
|
|
34
|
+
const CHANGE_LINE = 'change: pp-tools audience set <business|engineer|developer> [--language en | --clear-language]';
|
|
35
|
+
/** Cut to 40 characters and strip control characters before a value enters a message. */
|
|
36
|
+
function clip(v) {
|
|
37
|
+
return String(v).replace(/[\u0000-\u001f\u007f-\u009f]/g, '').slice(0, 40);
|
|
38
|
+
}
|
|
39
|
+
/** The project's `response_language`, or null when there is none (or no project here). */
|
|
40
|
+
function projectResponseLanguage(cwd) {
|
|
41
|
+
try {
|
|
42
|
+
const rl = configLoader.loadConfig(cwd).response_language;
|
|
43
|
+
if (typeof rl === 'string' && rl.trim() !== '')
|
|
44
|
+
return rl;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// no project here: the project tier is simply absent
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
function buildAudienceStatus(cwd, env = process.env) {
|
|
52
|
+
const person = (0, audience_cjs_1.readPerson)(env);
|
|
53
|
+
const aud = (0, audience_cjs_1.resolveAudience)(env, person);
|
|
54
|
+
const projectLanguage = projectResponseLanguage(cwd);
|
|
55
|
+
const lang = (0, audience_cjs_1.resolveLanguage)({ env, person, projectResponseLanguage: projectLanguage });
|
|
56
|
+
const choice = (0, audience_cjs_1.needsChoice)(env, person);
|
|
57
|
+
const status = {
|
|
58
|
+
audience: aud.audience,
|
|
59
|
+
audience_source: aud.source,
|
|
60
|
+
language: lang.code,
|
|
61
|
+
language_name: lang.name,
|
|
62
|
+
language_source: lang.source,
|
|
63
|
+
person_file: (0, audience_cjs_1.personFilePath)(env),
|
|
64
|
+
person_file_exists: person.exists,
|
|
65
|
+
needs_choice: choice,
|
|
66
|
+
warnings: aud.warnings,
|
|
67
|
+
};
|
|
68
|
+
if (choice)
|
|
69
|
+
status.prompt = audience_cjs_1.FIRST_RUN_PROMPT;
|
|
70
|
+
return status;
|
|
71
|
+
}
|
|
72
|
+
const AUDIENCE_LABEL = {
|
|
73
|
+
environment: 'from PP_AUDIENCE',
|
|
74
|
+
person_file: 'from person file',
|
|
75
|
+
default: 'default — nothing set',
|
|
76
|
+
};
|
|
77
|
+
const LANGUAGE_LABEL = {
|
|
78
|
+
person_file: 'from person file',
|
|
79
|
+
audience: 'from audience',
|
|
80
|
+
project: 'from project response_language',
|
|
81
|
+
};
|
|
82
|
+
function languageLine(s) {
|
|
83
|
+
if (s.language_source === 'none')
|
|
84
|
+
return 'language: not set';
|
|
85
|
+
return `language: ${String(s.language_name)} (${s.language ?? '-'}, ${LANGUAGE_LABEL[s.language_source]})`;
|
|
86
|
+
}
|
|
87
|
+
function humanStatus(s) {
|
|
88
|
+
return [
|
|
89
|
+
`audience: ${s.audience} (${AUDIENCE_LABEL[s.audience_source]})`,
|
|
90
|
+
languageLine(s),
|
|
91
|
+
`file: ${s.person_file} (${s.person_file_exists ? 'present' : 'absent'})`,
|
|
92
|
+
CHANGE_LINE,
|
|
93
|
+
].join('\n');
|
|
94
|
+
}
|
|
95
|
+
function warn(text) {
|
|
96
|
+
process.stderr.write(`pp-tools: warning: ${text}\n`);
|
|
97
|
+
}
|
|
98
|
+
function cmdStatus(cwd, raw) {
|
|
99
|
+
const status = buildAudienceStatus(cwd);
|
|
100
|
+
for (const w of status.warnings)
|
|
101
|
+
warn(w);
|
|
102
|
+
if (raw)
|
|
103
|
+
output(status, false);
|
|
104
|
+
else
|
|
105
|
+
output(status, true, humanStatus(status));
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Phase 40 CR-01: the audience fields an `init.*` payload carries, before any init call. A
|
|
109
|
+
* business user or business engineer gets one JSON object (`audience`, `audience_ref`,
|
|
110
|
+
* `response_language`) built by the same code as the payload; a developer gets no output at
|
|
111
|
+
* all, so a developer run loads nothing new (D-07). Workflows that ask the person something
|
|
112
|
+
* before their first init call run this first. Review iteration 2, WR-02: extra arguments are
|
|
113
|
+
* refused with a usage error, so a malformed call exits non-zero instead of looking like a
|
|
114
|
+
* developer run.
|
|
115
|
+
*/
|
|
116
|
+
function cmdFields(cwd, args) {
|
|
117
|
+
if (args.length > 0)
|
|
118
|
+
error('Usage: pp-tools audience fields (takes no arguments)', ERROR_REASON.USAGE);
|
|
119
|
+
const fields = initMod.audienceFields(projectResponseLanguage(cwd));
|
|
120
|
+
if (Object.keys(fields).length > 0)
|
|
121
|
+
output(fields, false);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Validate-then-write shared by `set` and `answer`; everything is checked before
|
|
125
|
+
* anything is written. `language` undefined keeps the stored language (CR-01).
|
|
126
|
+
*/
|
|
127
|
+
function save(cwd, audience, language, raw) {
|
|
128
|
+
const result = (0, audience_cjs_1.writePerson)(language === undefined ? { audience } : { audience, language });
|
|
129
|
+
if (!result.ok) {
|
|
130
|
+
error(`Could not save ${result.path}: ${result.error}`, ERROR_REASON.AUDIENCE_WRITE_FAILED);
|
|
131
|
+
}
|
|
132
|
+
const envOverride = (process.env['PP_AUDIENCE'] || '').trim().toLowerCase();
|
|
133
|
+
if ((0, audience_cjs_1.isAudience)(envOverride))
|
|
134
|
+
warn(`PP_AUDIENCE=${envOverride} overrides the saved value in this shell`);
|
|
135
|
+
const status = buildAudienceStatus(cwd);
|
|
136
|
+
for (const w of status.warnings)
|
|
137
|
+
warn(w);
|
|
138
|
+
if (raw) {
|
|
139
|
+
output({ ...status, saved: true }, false);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const langText = status.language_source === 'person_file' ? `${status.language ?? '-'}` : 'not set';
|
|
143
|
+
output({ ...status, saved: true }, true, `Saved: audience ${audience} (language: ${langText}) in ${result.path}\n${CHANGE_LINE}`);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* WR-11: `set --clear-language` without an audience removes only the stored
|
|
147
|
+
* language. The stored audience is never read back and re-saved, so a
|
|
148
|
+
* `PP_AUDIENCE` override (D-12) or a default audience cannot leak into the file.
|
|
149
|
+
*/
|
|
150
|
+
function clearLanguageOnly(cwd, raw) {
|
|
151
|
+
const result = (0, audience_cjs_1.writePerson)({ language: null });
|
|
152
|
+
if (!result.ok) {
|
|
153
|
+
error(`Could not save ${result.path}: ${result.error}`, ERROR_REASON.AUDIENCE_WRITE_FAILED);
|
|
154
|
+
}
|
|
155
|
+
const status = buildAudienceStatus(cwd);
|
|
156
|
+
for (const w of status.warnings)
|
|
157
|
+
warn(w);
|
|
158
|
+
const payload = { ...status, saved: result.written };
|
|
159
|
+
if (raw) {
|
|
160
|
+
output(payload, false);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const text = result.written
|
|
164
|
+
? `Cleared the language in ${result.path}; the audience decides it again (audience unchanged).`
|
|
165
|
+
: `No language is saved in ${result.path}; nothing changed.`;
|
|
166
|
+
output(payload, true, `${text}\n${CHANGE_LINE}`);
|
|
167
|
+
}
|
|
168
|
+
function cmdSet(cwd, args, raw) {
|
|
169
|
+
let value = null;
|
|
170
|
+
let language = null;
|
|
171
|
+
let languageGiven = false;
|
|
172
|
+
let clearLanguage = false;
|
|
173
|
+
for (let i = 0; i < args.length; i++) {
|
|
174
|
+
const a = String(args[i]);
|
|
175
|
+
if (a === '--clear-language') {
|
|
176
|
+
clearLanguage = true;
|
|
177
|
+
}
|
|
178
|
+
else if (a === '--language') {
|
|
179
|
+
languageGiven = true;
|
|
180
|
+
const next = args[i + 1];
|
|
181
|
+
if (next === undefined || next.startsWith('--'))
|
|
182
|
+
error(SET_USAGE, ERROR_REASON.USAGE);
|
|
183
|
+
language = String(next);
|
|
184
|
+
i += 1;
|
|
185
|
+
}
|
|
186
|
+
else if (a.startsWith('--language=')) {
|
|
187
|
+
languageGiven = true;
|
|
188
|
+
language = a.slice('--language='.length);
|
|
189
|
+
}
|
|
190
|
+
else if (value === null) {
|
|
191
|
+
value = a;
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
error(SET_USAGE, ERROR_REASON.USAGE);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (value === null) {
|
|
198
|
+
if (clearLanguage && !languageGiven) {
|
|
199
|
+
clearLanguageOnly(cwd, raw);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
error(SET_USAGE, ERROR_REASON.USAGE);
|
|
203
|
+
}
|
|
204
|
+
const audience = value.trim().toLowerCase();
|
|
205
|
+
if (!(0, audience_cjs_1.isAudience)(audience)) {
|
|
206
|
+
error(`Unknown audience "${clip(value)}". Use one of: ${AUDIENCE_LIST}.`, ERROR_REASON.USAGE);
|
|
207
|
+
}
|
|
208
|
+
if (languageGiven && clearLanguage)
|
|
209
|
+
error(SET_USAGE, ERROR_REASON.USAGE);
|
|
210
|
+
// D-10 / CR-01: without --language the stored language is kept (undefined);
|
|
211
|
+
// --clear-language removes it so the audience decides again.
|
|
212
|
+
let lang = clearLanguage ? null : undefined;
|
|
213
|
+
if (languageGiven) {
|
|
214
|
+
const l = (language ?? '').trim().toLowerCase();
|
|
215
|
+
if (l === '')
|
|
216
|
+
error(SET_USAGE, ERROR_REASON.USAGE);
|
|
217
|
+
if (!(0, audience_cjs_1.isLanguage)(l)) {
|
|
218
|
+
error(`Unknown language "${clip(language)}". Use one of: ${LANGUAGE_LIST}.`, ERROR_REASON.USAGE);
|
|
219
|
+
}
|
|
220
|
+
lang = l;
|
|
221
|
+
}
|
|
222
|
+
save(cwd, audience, lang, raw);
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* The first-run relay seam (D-06). It only ever fills an empty setting: a chat
|
|
226
|
+
* relay cannot send Enter on purpose, so a missing or blank reply is refused
|
|
227
|
+
* rather than read as `developer` (Enter-means-developer lives only in the
|
|
228
|
+
* installer's TTY prompt), and an existing valid audience is never overwritten
|
|
229
|
+
* (WR-01). Changing a saved audience is `audience set`.
|
|
230
|
+
*/
|
|
231
|
+
function cmdAnswer(cwd, args, raw) {
|
|
232
|
+
if (args.length === 0)
|
|
233
|
+
error('Usage: pp-tools audience answer <reply>', ERROR_REASON.USAGE);
|
|
234
|
+
const person = (0, audience_cjs_1.readPerson)();
|
|
235
|
+
if (person.audience !== null) {
|
|
236
|
+
error(`An audience is already saved (${person.audience}). Change it with: pp-tools audience set <${audience_cjs_1.AUDIENCES.join('|')}>`, ERROR_REASON.USAGE);
|
|
237
|
+
}
|
|
238
|
+
// WR-10: join every argv word, so an unquoted "Business engineer" is read
|
|
239
|
+
// whole and quoting by the relay is not load-bearing.
|
|
240
|
+
const reply = args.map(String).join(' ');
|
|
241
|
+
if (reply.trim() === '')
|
|
242
|
+
error(`Answer 1, 2 or 3 (${AUDIENCE_LIST}).`, ERROR_REASON.USAGE);
|
|
243
|
+
const audience = (0, audience_cjs_1.parseAudienceInput)(reply);
|
|
244
|
+
if (audience === null) {
|
|
245
|
+
error(`Not recognised: "${clip(reply)}". Answer 1, 2 or 3 (${AUDIENCE_LIST}).`, ERROR_REASON.USAGE);
|
|
246
|
+
}
|
|
247
|
+
save(cwd, audience, undefined, raw);
|
|
248
|
+
}
|
|
249
|
+
function cmdAudience(cwd, args, raw) {
|
|
250
|
+
const sub = args[0];
|
|
251
|
+
const rest = args.slice(1);
|
|
252
|
+
switch (sub) {
|
|
253
|
+
case 'status':
|
|
254
|
+
cmdStatus(cwd, raw);
|
|
255
|
+
return;
|
|
256
|
+
case 'set':
|
|
257
|
+
cmdSet(cwd, rest, raw);
|
|
258
|
+
return;
|
|
259
|
+
case 'answer':
|
|
260
|
+
cmdAnswer(cwd, rest, raw);
|
|
261
|
+
return;
|
|
262
|
+
case 'fields':
|
|
263
|
+
cmdFields(cwd, rest);
|
|
264
|
+
return;
|
|
265
|
+
case undefined:
|
|
266
|
+
error('Usage: pp-tools audience <status|set|answer|fields> ...', ERROR_REASON.USAGE);
|
|
267
|
+
return;
|
|
268
|
+
default:
|
|
269
|
+
error(`Unknown audience subcommand: ${clip(sub)}. Available: status, set, answer, fields`, ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Per-person audience setting (Phase 39: AUD-01, AUD-04).
|
|
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/audience.cjs`; do NOT hand-edit
|
|
7
|
+
* the compiled output.
|
|
8
|
+
*
|
|
9
|
+
* This leaf is the ONLY reader and writer of the person file
|
|
10
|
+
* (`~/.gsd/person.json`, or under `GSD_HOME` when set; D-01). It imports Node
|
|
11
|
+
* built-ins only, so `io.cts` and the hooks can require it later without an
|
|
12
|
+
* import cycle. The locale (`LANG`, `LC_*`) is never read (D-02): a Dutch
|
|
13
|
+
* laptop must not silently change output for current users. The person file
|
|
14
|
+
* never enters `.planning/config.json` or `~/.gsd/defaults.json` (D-01).
|
|
15
|
+
*
|
|
16
|
+
* Audience order: `PP_AUDIENCE` env, then the person file, then `developer`.
|
|
17
|
+
* Language order (D-04): the person's explicit language, then the language the
|
|
18
|
+
* audience implies (business en, engineer en, developer none), then the
|
|
19
|
+
* project's `response_language`. English only since 2026-11-13 (operator: Dutch
|
|
20
|
+
* dropped from v1.23.0); the list shape allows adding a language later.
|
|
21
|
+
*
|
|
22
|
+
* Nothing here throws. Readers return a value plus `warnings`; the CLI adapter
|
|
23
|
+
* prints them.
|
|
24
|
+
*/
|
|
25
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
26
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
27
|
+
};
|
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
+
exports.FIRST_RUN_PROMPT = exports.PERSON_FILE_MAX_BYTES = exports.PERSON_FILE_VERSION = exports.LANGUAGE_NAMES = exports.IMPLIED_LANGUAGE = exports.LANGUAGES = exports.AUDIENCES = void 0;
|
|
30
|
+
exports.isAudience = isAudience;
|
|
31
|
+
exports.isLanguage = isLanguage;
|
|
32
|
+
exports.personFilePath = personFilePath;
|
|
33
|
+
exports.readPerson = readPerson;
|
|
34
|
+
exports.writePerson = writePerson;
|
|
35
|
+
exports.resolveAudience = resolveAudience;
|
|
36
|
+
exports.resolveLanguage = resolveLanguage;
|
|
37
|
+
exports.needsChoice = needsChoice;
|
|
38
|
+
exports.parseAudienceInput = parseAudienceInput;
|
|
39
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
40
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
41
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
42
|
+
exports.AUDIENCES = ['business', 'engineer', 'developer'];
|
|
43
|
+
exports.LANGUAGES = ['en'];
|
|
44
|
+
/** The language each audience implies; `developer` implies none (D-04). */
|
|
45
|
+
exports.IMPLIED_LANGUAGE = {
|
|
46
|
+
business: 'en',
|
|
47
|
+
engineer: 'en',
|
|
48
|
+
developer: null,
|
|
49
|
+
};
|
|
50
|
+
exports.LANGUAGE_NAMES = { en: 'English' };
|
|
51
|
+
exports.PERSON_FILE_VERSION = 1;
|
|
52
|
+
exports.PERSON_FILE_MAX_BYTES = 4096;
|
|
53
|
+
/**
|
|
54
|
+
* The first-run question (D-06, D-09): plain English (Dutch dropped 2026-11-13).
|
|
55
|
+
* Rendered from this single constant by the installer and (via
|
|
56
|
+
* `audience status --raw`) /pp-init and /pp-discovery. /pp-settings asks it as
|
|
57
|
+
* an AskUserQuestion with these same labels and descriptions, pinned by
|
|
58
|
+
* tests/feat-2527-settings-layers.test.cjs (WR-06).
|
|
59
|
+
*/
|
|
60
|
+
exports.FIRST_RUN_PROMPT = [
|
|
61
|
+
'Who are you?',
|
|
62
|
+
'This decides how pp-core talks to you.',
|
|
63
|
+
'',
|
|
64
|
+
' 1) Business user',
|
|
65
|
+
' Plain words, no technical terms.',
|
|
66
|
+
' 2) Business engineer',
|
|
67
|
+
' Plain English; you know the technology.',
|
|
68
|
+
' 3) Developer',
|
|
69
|
+
' As today.',
|
|
70
|
+
'',
|
|
71
|
+
'Choose 1, 2 or 3 [3]:',
|
|
72
|
+
].join('\n') + '\n';
|
|
73
|
+
const AUDIENCE_LIST = exports.AUDIENCES.join(', ');
|
|
74
|
+
const LANGUAGE_LIST = exports.LANGUAGES.join(', ');
|
|
75
|
+
function isAudience(v) {
|
|
76
|
+
return typeof v === 'string' && exports.AUDIENCES.includes(v);
|
|
77
|
+
}
|
|
78
|
+
function isLanguage(v) {
|
|
79
|
+
return typeof v === 'string' && exports.LANGUAGES.includes(v);
|
|
80
|
+
}
|
|
81
|
+
/** Cut to 40 characters and strip control characters before a value enters a message. */
|
|
82
|
+
function clip(v) {
|
|
83
|
+
return String(v).replace(/[\u0000-\u001f\u007f-\u009f]/g, '').slice(0, 40);
|
|
84
|
+
}
|
|
85
|
+
function norm(v) {
|
|
86
|
+
return typeof v === 'string' ? v.trim().toLowerCase() : '';
|
|
87
|
+
}
|
|
88
|
+
/** Same root rule as config-loader's `_globalDefaultsPath`, so GSD_HOME redirects both. */
|
|
89
|
+
function personFilePath(env = process.env) {
|
|
90
|
+
return node_path_1.default.join(env['GSD_HOME'] || node_os_1.default.homedir(), '.gsd', 'person.json');
|
|
91
|
+
}
|
|
92
|
+
const POLLUTING_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
93
|
+
/** Parse the stored file tolerantly: any failure is reported as a warning, never thrown. */
|
|
94
|
+
function loadStored(file) {
|
|
95
|
+
let size;
|
|
96
|
+
try {
|
|
97
|
+
size = node_fs_1.default.statSync(file).size;
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return { obj: null, exists: false, warning: null };
|
|
101
|
+
}
|
|
102
|
+
if (size > exports.PERSON_FILE_MAX_BYTES) {
|
|
103
|
+
return { obj: null, exists: true, warning: `${file}: larger than ${exports.PERSON_FILE_MAX_BYTES} bytes — ignored` };
|
|
104
|
+
}
|
|
105
|
+
let parsed;
|
|
106
|
+
try {
|
|
107
|
+
parsed = JSON.parse(node_fs_1.default.readFileSync(file, 'utf8'));
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return { obj: null, exists: true, warning: `${file}: not valid JSON — ignored` };
|
|
111
|
+
}
|
|
112
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
113
|
+
return { obj: null, exists: true, warning: `${file}: not a JSON object — ignored` };
|
|
114
|
+
}
|
|
115
|
+
return { obj: parsed, exists: true, warning: null };
|
|
116
|
+
}
|
|
117
|
+
function readPerson(env = process.env) {
|
|
118
|
+
const person = { audience: null, language: null, exists: false, warnings: [] };
|
|
119
|
+
try {
|
|
120
|
+
const file = personFilePath(env);
|
|
121
|
+
const stored = loadStored(file);
|
|
122
|
+
person.exists = stored.exists;
|
|
123
|
+
if (stored.warning)
|
|
124
|
+
person.warnings.push(stored.warning);
|
|
125
|
+
const obj = stored.obj;
|
|
126
|
+
if (!obj)
|
|
127
|
+
return person;
|
|
128
|
+
const own = (k) => Object.prototype.hasOwnProperty.call(obj, k);
|
|
129
|
+
if (own('audience')) {
|
|
130
|
+
const a = norm(obj['audience']);
|
|
131
|
+
if (isAudience(a))
|
|
132
|
+
person.audience = a;
|
|
133
|
+
else
|
|
134
|
+
person.warnings.push(`${file}: audience "${clip(obj['audience'])}" is not one of ${AUDIENCE_LIST} — ignored`);
|
|
135
|
+
}
|
|
136
|
+
if (own('language')) {
|
|
137
|
+
const l = norm(obj['language']);
|
|
138
|
+
if (isLanguage(l))
|
|
139
|
+
person.language = l;
|
|
140
|
+
else
|
|
141
|
+
person.warnings.push(`${file}: language "${clip(obj['language'])}" is not one of ${LANGUAGE_LIST} — ignored`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
catch (e) {
|
|
145
|
+
person.warnings.push(`person file could not be read: ${clip(e.message)}`);
|
|
146
|
+
}
|
|
147
|
+
return person;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Save the audience. `language` is three-state (D-10, CR-01): a value sets the
|
|
151
|
+
* override, `null` removes it, and an omitted/`undefined` language keeps
|
|
152
|
+
* whatever is stored, so callers that never ask about language cannot erase it.
|
|
153
|
+
*
|
|
154
|
+
* WR-11: `{ language: null }` without an audience clears only the language and
|
|
155
|
+
* leaves the stored `audience` key exactly as it is, so a hint that clears the
|
|
156
|
+
* language never has to name (and so never overwrites) an audience. When there
|
|
157
|
+
* is no stored `language` key to remove, nothing is written (`written: false`):
|
|
158
|
+
* a language clear never creates a person file or rewrites one it cannot parse.
|
|
159
|
+
*/
|
|
160
|
+
function writePerson(update, env = process.env) {
|
|
161
|
+
const file = personFilePath(env);
|
|
162
|
+
let tmp = null;
|
|
163
|
+
try {
|
|
164
|
+
// Key-preserving read-modify-write: a whole-file rewrite that drops keys is
|
|
165
|
+
// the defect that ruled out defaults.json (D-01).
|
|
166
|
+
const next = {};
|
|
167
|
+
const stored = loadStored(file).obj;
|
|
168
|
+
if (update.audience === undefined && !(stored && Object.prototype.hasOwnProperty.call(stored, 'language'))) {
|
|
169
|
+
return { ok: true, path: file, written: false };
|
|
170
|
+
}
|
|
171
|
+
const dir = node_path_1.default.dirname(file);
|
|
172
|
+
node_fs_1.default.mkdirSync(dir, { recursive: true });
|
|
173
|
+
if (stored) {
|
|
174
|
+
for (const k of Object.keys(stored)) {
|
|
175
|
+
if (!POLLUTING_KEYS.has(k))
|
|
176
|
+
next[k] = stored[k];
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
next['version'] = exports.PERSON_FILE_VERSION;
|
|
180
|
+
if (update.audience !== undefined)
|
|
181
|
+
next['audience'] = update.audience;
|
|
182
|
+
if (update.language)
|
|
183
|
+
next['language'] = update.language;
|
|
184
|
+
else if (update.language === null)
|
|
185
|
+
delete next['language'];
|
|
186
|
+
tmp = node_path_1.default.join(dir, `person.json.tmp-${process.pid}-${Math.random().toString(16).slice(2, 10)}`);
|
|
187
|
+
node_fs_1.default.writeFileSync(tmp, JSON.stringify(next, null, 2) + '\n');
|
|
188
|
+
node_fs_1.default.renameSync(tmp, file);
|
|
189
|
+
return { ok: true, path: file, written: true };
|
|
190
|
+
}
|
|
191
|
+
catch (e) {
|
|
192
|
+
if (tmp) {
|
|
193
|
+
try {
|
|
194
|
+
node_fs_1.default.unlinkSync(tmp);
|
|
195
|
+
}
|
|
196
|
+
catch { /* best effort */ }
|
|
197
|
+
}
|
|
198
|
+
return { ok: false, path: file, error: e.message };
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
/** The valid PP_AUDIENCE value, or null; an invalid non-empty value is reported via `warnings`. */
|
|
202
|
+
function readEnvAudience(env) {
|
|
203
|
+
const raw = env['PP_AUDIENCE'];
|
|
204
|
+
const v = norm(raw);
|
|
205
|
+
if (v === '')
|
|
206
|
+
return { value: null, warning: null };
|
|
207
|
+
if (isAudience(v))
|
|
208
|
+
return { value: v, warning: null };
|
|
209
|
+
return { value: null, warning: `PP_AUDIENCE "${clip(raw)}" is not one of ${AUDIENCE_LIST} — ignored` };
|
|
210
|
+
}
|
|
211
|
+
function resolveAudience(env, person) {
|
|
212
|
+
const fromEnv = readEnvAudience(env);
|
|
213
|
+
const warnings = [];
|
|
214
|
+
if (fromEnv.warning)
|
|
215
|
+
warnings.push(fromEnv.warning);
|
|
216
|
+
warnings.push(...person.warnings);
|
|
217
|
+
if (fromEnv.value)
|
|
218
|
+
return { audience: fromEnv.value, source: 'environment', warnings };
|
|
219
|
+
if (person.audience)
|
|
220
|
+
return { audience: person.audience, source: 'person_file', warnings };
|
|
221
|
+
return { audience: 'developer', source: 'default', warnings };
|
|
222
|
+
}
|
|
223
|
+
function resolveLanguage(input) {
|
|
224
|
+
const { env, person, projectResponseLanguage } = input;
|
|
225
|
+
if (person.language) {
|
|
226
|
+
return { code: person.language, name: exports.LANGUAGE_NAMES[person.language], source: 'person_file' };
|
|
227
|
+
}
|
|
228
|
+
const { audience } = resolveAudience(env, person);
|
|
229
|
+
const implied = exports.IMPLIED_LANGUAGE[audience];
|
|
230
|
+
if (implied)
|
|
231
|
+
return { code: implied, name: exports.LANGUAGE_NAMES[implied], source: 'audience' };
|
|
232
|
+
if (typeof projectResponseLanguage === 'string' && projectResponseLanguage.trim() !== '') {
|
|
233
|
+
return { code: null, name: projectResponseLanguage, source: 'project' };
|
|
234
|
+
}
|
|
235
|
+
return { code: null, name: null, source: 'none' };
|
|
236
|
+
}
|
|
237
|
+
/** True only when no valid setting exists anywhere: no usable PP_AUDIENCE and no valid person file. */
|
|
238
|
+
function needsChoice(env, person) {
|
|
239
|
+
return readEnvAudience(env).value === null && person.audience === null;
|
|
240
|
+
}
|
|
241
|
+
const NUMBERED_AUDIENCE = new Map([
|
|
242
|
+
['1', 'business'],
|
|
243
|
+
['2', 'engineer'],
|
|
244
|
+
['3', 'developer'],
|
|
245
|
+
]);
|
|
246
|
+
/** Option labels (D-09); lower-case, single spaces. */
|
|
247
|
+
const NAMED_AUDIENCE = new Map([
|
|
248
|
+
['business', 'business'],
|
|
249
|
+
['business user', 'business'],
|
|
250
|
+
['engineer', 'engineer'],
|
|
251
|
+
['business engineer', 'engineer'],
|
|
252
|
+
['developer', 'developer'],
|
|
253
|
+
]);
|
|
254
|
+
/**
|
|
255
|
+
* Map a first-run reply to an audience. Enter or whitespace means `developer`;
|
|
256
|
+
* `null`/`undefined` (no reply at all) and anything unrecognised give null.
|
|
257
|
+
*
|
|
258
|
+
* Replies are normalised before matching (WR-05), because a relaying agent
|
|
259
|
+
* most often echoes the chosen option line: a leading option number
|
|
260
|
+
* (`2) Business engineer`), a trailing `(3)`, and each half of a
|
|
261
|
+
* `a / b` label are all read. Every part that names an audience
|
|
262
|
+
* must agree; unknown text or a contradiction (`1) Business engineer`) gives
|
|
263
|
+
* null rather than a guess.
|
|
264
|
+
*/
|
|
265
|
+
function parseAudienceInput(answer) {
|
|
266
|
+
if (typeof answer !== 'string' && typeof answer !== 'number')
|
|
267
|
+
return null;
|
|
268
|
+
const s = String(answer).trim().toLowerCase();
|
|
269
|
+
if (s === '')
|
|
270
|
+
return 'developer';
|
|
271
|
+
const found = new Set();
|
|
272
|
+
let rest = s;
|
|
273
|
+
const lead = /^([123])(?![0-9])[).:\s-]*/.exec(rest);
|
|
274
|
+
if (lead) {
|
|
275
|
+
found.add(NUMBERED_AUDIENCE.get(lead[1]));
|
|
276
|
+
rest = rest.slice(lead[0].length);
|
|
277
|
+
}
|
|
278
|
+
const trail = /\s*\(([123])\)$/.exec(rest);
|
|
279
|
+
if (trail) {
|
|
280
|
+
found.add(NUMBERED_AUDIENCE.get(trail[1]));
|
|
281
|
+
rest = rest.slice(0, trail.index);
|
|
282
|
+
}
|
|
283
|
+
for (const half of rest.split('/')) {
|
|
284
|
+
const label = half.trim().replace(/[).:!]+$/, '').replace(/\s+/g, ' ');
|
|
285
|
+
if (label === '')
|
|
286
|
+
continue;
|
|
287
|
+
const named = NAMED_AUDIENCE.get(label);
|
|
288
|
+
if (!named)
|
|
289
|
+
return null;
|
|
290
|
+
found.add(named);
|
|
291
|
+
}
|
|
292
|
+
return found.size === 1 ? [...found][0] : null;
|
|
293
|
+
}
|
|
@@ -47,6 +47,8 @@ const { extractFrontmatter } = frontmatter;
|
|
|
47
47
|
const modelProfiles = require("./model-profiles.cjs");
|
|
48
48
|
const { MODEL_PROFILES, VALID_PHASE_TYPES } = modelProfiles;
|
|
49
49
|
const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
|
|
50
|
+
const messages_cjs_1 = require("./messages.cjs");
|
|
51
|
+
const message_labels_cjs_1 = require("./message-labels.cjs");
|
|
50
52
|
// 16-03: the one `.planning/` path test. The seal leaf's top level imports only
|
|
51
53
|
// Node built-ins and it never requires this module, so the edge adds no cycle.
|
|
52
54
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
@@ -909,17 +911,20 @@ function cmdProgressRender(cwd, format, raw) {
|
|
|
909
911
|
catch { /* intentionally empty */ }
|
|
910
912
|
const percent = totalPlans > 0 ? Math.min(100, Math.round((totalSummaries / totalPlans) * 100)) : 0;
|
|
911
913
|
if (format === 'table') {
|
|
912
|
-
// Render markdown table
|
|
914
|
+
// Render markdown table. Display text only: the status cell is the canonical
|
|
915
|
+
// word relabelled here, at the last step, and nothing reads it back (D-13).
|
|
916
|
+
const audience = (0, messages_cjs_1.audienceOf)(process.env);
|
|
913
917
|
const barWidth = 10;
|
|
914
918
|
const filled = Math.round((percent / 100) * barWidth);
|
|
915
919
|
const bar = '█'.repeat(filled) + '░'.repeat(barWidth - filled);
|
|
916
|
-
let out =
|
|
917
|
-
out +=
|
|
918
|
-
out +=
|
|
919
|
-
out += `|-------|------|-------|--------|\n`;
|
|
920
|
+
let out = (0, messages_cjs_1.text)('progress.table', { version: milestone.version, name: milestone.name }, { audience }) + '\n\n';
|
|
921
|
+
out += (0, messages_cjs_1.text)('progress.table.progress', { bar, done: totalSummaries, total: totalPlans, percent }, { audience }) + '\n\n';
|
|
922
|
+
out += (0, messages_cjs_1.text)('progress.table.header', {}, { audience }) + '\n';
|
|
920
923
|
for (const p of phases) {
|
|
921
|
-
out += `| ${p.number} | ${p.name} | ${p.summaries}/${p.plans} | ${p.status} |\n`;
|
|
924
|
+
out += `| ${p.number} | ${p.name} | ${p.summaries}/${p.plans} | ${(0, message_labels_cjs_1.label)('phase.status', p.status, { audience })} |\n`;
|
|
922
925
|
}
|
|
926
|
+
if (audience !== 'developer')
|
|
927
|
+
out += (0, messages_cjs_1.refLine)('progress.table') + '\n';
|
|
923
928
|
output({ rendered: out }, raw, out);
|
|
924
929
|
}
|
|
925
930
|
else if (format === 'bar') {
|
|
@@ -1279,32 +1284,36 @@ function cmdStats(cwd, format, raw) {
|
|
|
1279
1284
|
last_activity: lastActivity,
|
|
1280
1285
|
};
|
|
1281
1286
|
if (format === 'table') {
|
|
1287
|
+
// Display text only: the status cell is the canonical word relabelled here,
|
|
1288
|
+
// at the last step; completedPhases above is counted on the canonical word (D-13).
|
|
1289
|
+
const audience = (0, messages_cjs_1.audienceOf)(process.env);
|
|
1282
1290
|
const barWidth = 10;
|
|
1283
1291
|
const filled = Math.round((percent / 100) * barWidth);
|
|
1284
1292
|
const bar = '█'.repeat(filled) + '░'.repeat(barWidth - filled);
|
|
1285
|
-
let out =
|
|
1286
|
-
out +=
|
|
1293
|
+
let out = (0, messages_cjs_1.text)('stats.table', { version: milestone.version, name: milestone.name }, { audience }) + '\n\n';
|
|
1294
|
+
out += (0, messages_cjs_1.text)('stats.table.progress', { bar, done: completedPhases, total: phases.length, percent }, { audience }) + '\n';
|
|
1287
1295
|
if (totalPlans > 0) {
|
|
1288
|
-
out +=
|
|
1296
|
+
out += (0, messages_cjs_1.text)('stats.table.plans', { done: totalSummaries, total: totalPlans, percent: planPercent }, { audience }) + '\n';
|
|
1289
1297
|
}
|
|
1290
|
-
out +=
|
|
1298
|
+
out += (0, messages_cjs_1.text)('stats.table.phases', { done: completedPhases, total: phases.length }, { audience }) + '\n';
|
|
1291
1299
|
if (requirementsTotal > 0) {
|
|
1292
|
-
out +=
|
|
1300
|
+
out += (0, messages_cjs_1.text)('stats.table.requirements', { done: requirementsComplete, total: requirementsTotal }, { audience }) + '\n';
|
|
1293
1301
|
}
|
|
1294
1302
|
out += '\n';
|
|
1295
|
-
out +=
|
|
1296
|
-
out += `|-------|------|-------|-----------|--------|------------|\n`;
|
|
1303
|
+
out += (0, messages_cjs_1.text)('stats.table.header', {}, { audience }) + '\n';
|
|
1297
1304
|
for (const p of phases) {
|
|
1298
|
-
out += `| ${p.number} | ${p.name} | ${p.plans} | ${p.summaries} | ${p.status} | ${p.wall_clock ?? '—'} |\n`;
|
|
1305
|
+
out += `| ${p.number} | ${p.name} | ${p.plans} | ${p.summaries} | ${(0, message_labels_cjs_1.label)('phase.status', p.status, { audience })} | ${p.wall_clock ?? '—'} |\n`;
|
|
1299
1306
|
}
|
|
1300
1307
|
if (gitCommits > 0) {
|
|
1301
|
-
out +=
|
|
1308
|
+
out += '\n' + (0, messages_cjs_1.text)('stats.table.git', { count: gitCommits }, { audience });
|
|
1302
1309
|
if (gitFirstCommitDate)
|
|
1303
|
-
out +=
|
|
1310
|
+
out += (0, messages_cjs_1.text)('stats.table.git_since', { date: gitFirstCommitDate }, { audience });
|
|
1304
1311
|
out += '\n';
|
|
1305
1312
|
}
|
|
1306
1313
|
if (lastActivity)
|
|
1307
|
-
out +=
|
|
1314
|
+
out += (0, messages_cjs_1.text)('stats.table.last_activity', { date: lastActivity }, { audience }) + '\n';
|
|
1315
|
+
if (audience !== 'developer')
|
|
1316
|
+
out += (0, messages_cjs_1.refLine)('stats.table') + '\n';
|
|
1308
1317
|
output({ rendered: out }, raw, out);
|
|
1309
1318
|
}
|
|
1310
1319
|
else {
|