@pareto4/pp-core 1.18.0 → 1.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/agents/pp-code-fixer.md +9 -5
- package/agents/pp-code-reviewer.md +1 -0
- package/agents/pp-codebase-mapper.md +3 -0
- package/agents/pp-debugger.md +3 -2
- package/agents/pp-executor.md +16 -7
- package/agents/pp-integration-checker.md +1 -0
- package/agents/pp-phase-researcher.md +13 -1
- package/agents/pp-plan-checker.md +3 -0
- package/agents/pp-planner.md +20 -1
- package/agents/pp-research-synthesizer.md +1 -1
- package/agents/pp-ui-auditor.md +1 -0
- package/agents/pp-ui-researcher.md +3 -1
- package/agents/pp-vault-reviewer.md +2 -0
- package/agents/pp-verifier.md +22 -10
- package/commands/pp-add-tests.md +1 -1
- package/commands/pp-debug.md +1 -0
- package/commands/pp-discovery.md +3 -2
- package/commands/pp-init.md +4 -3
- package/commands/pp-quick.md +3 -0
- package/commands/pp-review-backlog.md +2 -1
- package/gemini-extension.json +1 -1
- package/package.json +6 -3
- package/pareto-skills/_shared/pp-claude-md-protocol.md +1 -1
- package/pareto-skills/pp-project-setup/SKILL.md +18 -343
- package/pp-core/bin/lib/capability-registry.cjs +3 -3
- package/pp-core/bin/lib/check-command-router.cjs +60 -20
- package/pp-core/bin/lib/commands.cjs +79 -4
- package/pp-core/bin/lib/compose-bindings.cjs +97 -14
- package/pp-core/bin/lib/discovery-access.cjs +276 -0
- package/pp-core/bin/lib/discovery-charter.cjs +664 -0
- package/pp-core/bin/lib/discovery-deviations.cjs +264 -0
- package/pp-core/bin/lib/discovery-entities.cjs +323 -0
- package/pp-core/bin/lib/discovery-for-phase.cjs +628 -0
- package/pp-core/bin/lib/discovery-integrations.cjs +286 -0
- package/pp-core/bin/lib/discovery-migration.cjs +246 -0
- package/pp-core/bin/lib/discovery-phases.cjs +233 -0
- package/pp-core/bin/lib/discovery-processes.cjs +355 -0
- package/pp-core/bin/lib/discovery-research-track.cjs +5 -4
- package/pp-core/bin/lib/discovery-rollup.cjs +12 -8
- package/pp-core/bin/lib/discovery-rules.cjs +216 -0
- package/pp-core/bin/lib/discovery-screens.cjs +21 -3
- package/pp-core/bin/lib/discovery-session-check.cjs +264 -0
- package/pp-core/bin/lib/discovery-stories.cjs +273 -0
- package/pp-core/bin/lib/discovery-sweep.cjs +13 -4
- package/pp-core/bin/lib/discovery-table.cjs +55 -0
- package/pp-core/bin/lib/discovery-vault-check.cjs +37 -3
- package/pp-core/bin/lib/init-contract.cjs +48 -7
- package/pp-core/bin/lib/io.cjs +22 -0
- package/pp-core/bin/lib/vault-coordinates.cjs +1 -1
- package/pp-core/bin/lib/vault-index.cjs +18 -5
- package/pp-core/bin/lib/vault-standards-review.cjs +50 -2
- package/pp-core/bin/lib/vault-update.cjs +4 -0
- package/pp-core/bin/lib/vault.cjs +2 -2
- package/pp-core/bin/pp-tools.cjs +153 -5
- package/pp-core/org-profile.json +1 -1
- package/pp-core/references/autonomous-best-effort.md +3 -2
- package/pp-core/references/autonomous-lifecycle.md +1 -0
- package/pp-core/references/autonomous-smart-discuss.md +5 -1
- package/pp-core/references/discovery-access.md +112 -0
- package/pp-core/references/discovery-build-consumption.md +256 -0
- package/pp-core/references/discovery-charter.md +127 -0
- package/pp-core/references/discovery-data-model.md +145 -0
- package/pp-core/references/discovery-id-conventions.md +77 -8
- package/pp-core/references/discovery-integrations.md +151 -0
- package/pp-core/references/discovery-processes.md +190 -0
- package/pp-core/references/discovery-rebaseline.md +2 -2
- package/pp-core/references/discovery-research-track.md +5 -6
- package/pp-core/references/discovery-session-output.md +70 -0
- package/pp-core/references/discovery-standards.md +107 -0
- package/pp-core/references/discovery-traceability.md +2 -2
- package/pp-core/references/execute-mvp-tdd.md +3 -3
- package/pp-core/references/git-integration.md +57 -33
- package/pp-core/references/git-planning-commit.md +31 -9
- package/pp-core/references/phase-argument-parsing.md +1 -0
- package/pp-core/references/planner-discovery-extract.md +35 -0
- package/pp-core/references/planner-gap-closure.md +1 -0
- package/pp-core/references/planner-revision.md +3 -1
- package/pp-core/references/planner-tdd.md +4 -4
- package/pp-core/references/planning-config.md +3 -3
- package/pp-core/references/shell-portability.md +28 -0
- package/pp-core/references/tdd.md +20 -16
- package/pp-core/references/verifier-discovery-conformance.md +36 -0
- package/pp-core/references/worktree-path-safety.md +1 -0
- package/pp-core/templates/milestone.md +5 -5
- package/pp-core/templates/project-charter.md +161 -100
- package/pp-core/templates/project.md +3 -3
- package/pp-core/templates/requirements.md +18 -0
- package/pp-core/templates/verification-report.md +16 -0
- package/pp-core/workflows/add-backlog.md +1 -1
- package/pp-core/workflows/add-tests.md +5 -2
- package/pp-core/workflows/add-todo.md +3 -1
- package/pp-core/workflows/ai-integration-phase.md +2 -1
- package/pp-core/workflows/audit-fix.md +4 -3
- package/pp-core/workflows/audit-milestone.md +1 -0
- package/pp-core/workflows/autonomous.md +6 -0
- package/pp-core/workflows/check-todos.md +2 -1
- package/pp-core/workflows/cleanup.md +3 -1
- package/pp-core/workflows/code-review-fix.md +12 -5
- package/pp-core/workflows/code-review.md +15 -1
- package/pp-core/workflows/complete-milestone.md +13 -3
- package/pp-core/workflows/debug.md +2 -0
- package/pp-core/workflows/diagnose-issues.md +1 -1
- package/pp-core/workflows/discovery-sprint/charter.md +114 -0
- package/pp-core/workflows/discovery-sprint/prefill.md +1 -1
- package/pp-core/workflows/discovery-sprint/prototype.md +4 -4
- package/pp-core/workflows/discovery-sprint/session-13-gates.md +11 -1
- package/pp-core/workflows/discovery-sprint/sessions/01-kick-off.md +54 -7
- package/pp-core/workflows/discovery-sprint/sessions/02-as-is.md +31 -7
- package/pp-core/workflows/discovery-sprint/sessions/03-to-be.md +64 -37
- package/pp-core/workflows/discovery-sprint/sessions/04-ux-design.md +12 -3
- package/pp-core/workflows/discovery-sprint/sessions/05-architecture.md +45 -22
- package/pp-core/workflows/discovery-sprint/sessions/06-security.md +30 -15
- package/pp-core/workflows/discovery-sprint/sessions/07-identity.md +43 -25
- package/pp-core/workflows/discovery-sprint/sessions/08-integration.md +63 -24
- package/pp-core/workflows/discovery-sprint/sessions/09-qa.md +28 -13
- package/pp-core/workflows/discovery-sprint/sessions/10-infrastructure.md +35 -16
- package/pp-core/workflows/discovery-sprint/sessions/11-roadmap.md +32 -8
- package/pp-core/workflows/discovery-sprint/sessions/12-delivery-model.md +21 -18
- package/pp-core/workflows/discovery-sprint/sessions/13-wrap-up.md +29 -12
- package/pp-core/workflows/discovery-sprint/vault-context.md +14 -3
- package/pp-core/workflows/discovery-sprint.md +23 -23
- package/pp-core/workflows/discovery-to-build/steps/architecture-deliverables.md +81 -29
- package/pp-core/workflows/discovery-to-build/steps/{screen-inventory-gate.md → handoff-gates.md} +111 -3
- package/pp-core/workflows/discovery-to-build/templates/discovery-references.md +12 -9
- package/pp-core/workflows/discovery-to-build/templates/pp-handoff.md +27 -26
- package/pp-core/workflows/discovery-to-build.md +70 -34
- package/pp-core/workflows/discuss-phase/discovery.md +83 -0
- package/pp-core/workflows/discuss-phase/templates/context.md +4 -0
- package/pp-core/workflows/discuss-phase-assumptions.md +5 -3
- package/pp-core/workflows/discuss-phase.md +18 -10
- package/pp-core/workflows/docs-update.md +1 -0
- package/pp-core/workflows/eval-review.md +2 -1
- package/pp-core/workflows/execute-phase/steps/check-query-gate.md +7 -6
- package/pp-core/workflows/execute-phase/steps/cross-ai-delegation.md +1 -0
- package/pp-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +1 -0
- package/pp-core/workflows/execute-phase/steps/vault-fetch.md +12 -0
- package/pp-core/workflows/execute-phase.md +19 -7
- package/pp-core/workflows/execute-plan.md +18 -7
- package/pp-core/workflows/explore.md +1 -1
- package/pp-core/workflows/forensics.md +4 -2
- package/pp-core/workflows/health.md +1 -0
- package/pp-core/workflows/import.md +2 -1
- package/pp-core/workflows/ingest-docs.md +1 -0
- package/pp-core/workflows/init/steps/client-context-capture.md +1 -0
- package/pp-core/workflows/init/steps/github-provisioning.md +145 -0
- package/pp-core/workflows/init/steps/gitlab-provisioning.md +18 -4
- package/pp-core/workflows/init/steps/reachability-outcome.md +3 -1
- package/pp-core/workflows/init.md +56 -66
- package/pp-core/workflows/map-codebase.md +5 -1
- package/pp-core/workflows/milestone-summary.md +2 -1
- package/pp-core/workflows/mvp-phase.md +1 -0
- package/pp-core/workflows/new-milestone.md +9 -5
- package/pp-core/workflows/new-project.md +12 -11
- package/pp-core/workflows/new-workspace.md +1 -0
- package/pp-core/workflows/next.md +2 -1
- package/pp-core/workflows/pause-work.md +6 -4
- package/pp-core/workflows/plan-milestone-gaps.md +3 -1
- package/pp-core/workflows/plan-phase/steps/chunked-planning.md +1 -1
- package/pp-core/workflows/plan-phase/steps/discovery-extract.md +41 -0
- package/pp-core/workflows/plan-phase.md +36 -16
- package/pp-core/workflows/plan-review-convergence.md +2 -0
- package/pp-core/workflows/plant-seed.md +4 -2
- package/pp-core/workflows/pr-branch.md +2 -0
- package/pp-core/workflows/progress.md +5 -0
- package/pp-core/workflows/quick.md +11 -4
- package/pp-core/workflows/reapply-patches.md +3 -0
- package/pp-core/workflows/remove-phase.md +1 -1
- package/pp-core/workflows/resume-project.md +3 -0
- package/pp-core/workflows/review.md +7 -1
- package/pp-core/workflows/secure-phase.md +2 -1
- package/pp-core/workflows/session-report.md +1 -0
- package/pp-core/workflows/ship.md +8 -1
- package/pp-core/workflows/sketch-wrap-up.md +1 -1
- package/pp-core/workflows/sketch.md +3 -2
- package/pp-core/workflows/spec-phase.md +2 -1
- package/pp-core/workflows/spike-wrap-up.md +1 -1
- package/pp-core/workflows/spike.md +4 -3
- package/pp-core/workflows/sync-skills.md +5 -2
- package/pp-core/workflows/thread.md +3 -2
- package/pp-core/workflows/transition.md +6 -0
- package/pp-core/workflows/ui-phase.md +3 -1
- package/pp-core/workflows/ui-review.md +2 -1
- package/pp-core/workflows/ultraplan-phase.md +1 -0
- package/pp-core/workflows/undo.md +14 -10
- package/pp-core/workflows/update.md +5 -0
- package/pp-core/workflows/validate-phase.md +6 -3
- package/pp-core/workflows/vault-review.md +7 -1
- package/pp-core/workflows/vault-write-back.md +5 -0
- package/pp-core/workflows/verify-phase.md +5 -0
- package/pp-core/workflows/verify-work.md +8 -1
- package/scripts/prompt-injection-scan.sh +1 -1
- package/scripts/sync-runtime-launcher.cjs +115 -5
- package/pp-core/workflows/discovery-sprint/templates/context-template.md +0 -266
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Discovery user-stories check (Discovery Sprint improvement plan, Batch 4).
|
|
4
|
+
*
|
|
5
|
+
* `.planning/discovery/_stories.md` is the register of user stories Session 3
|
|
6
|
+
* Step 5E mints from the step matrices: `US-nn`, the EPIC it belongs to, the
|
|
7
|
+
* requirements it realises, the story itself, its acceptance criteria and the
|
|
8
|
+
* step it was derived from. Before Batch 4 the stories lived only in Session
|
|
9
|
+
* 3's Notes Template, where no build agent read them (review F-4); the
|
|
10
|
+
* verifier now takes its UAT from this register through `for-phase`. This
|
|
11
|
+
* module checks the structure:
|
|
12
|
+
*
|
|
13
|
+
* - the `## Stories` table exists with the template's columns;
|
|
14
|
+
* - `US-` IDs are well-formed, unique, contiguous;
|
|
15
|
+
* - `EPIC` is one `E-nn`;
|
|
16
|
+
* - every story cites at least one requirement, each existing in
|
|
17
|
+
* `_requirements.md` when it is on file and parseable;
|
|
18
|
+
* - `Story` and `Acceptance criteria` are non-empty;
|
|
19
|
+
* - `Acceptance criteria` is a criterion, not a pointer (`As in F-01`);
|
|
20
|
+
* - `Source step` is `—` or a comma list of `MP-nn #<cell>`, the main process
|
|
21
|
+
* and the step row existing in `_processes.md` when it is on file;
|
|
22
|
+
* - every MVP-disposition requirement that a persona-actor step cites has at
|
|
23
|
+
* least one story (the `N-`/`T-` prefixes never need one; a requirement
|
|
24
|
+
* only `System` steps realise, or no step at all, has no user to tell it).
|
|
25
|
+
*
|
|
26
|
+
* Posture (D-43/D-53): reports at Sessions 8 and 13; hard-blocks at
|
|
27
|
+
* `/pp-discovery-to-build`. Absent artifact → exit 0.
|
|
28
|
+
*/
|
|
29
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
30
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
31
|
+
};
|
|
32
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
33
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
34
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
35
|
+
const io = require("./io.cjs");
|
|
36
|
+
const { output, error, ERROR_REASON } = io;
|
|
37
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
38
|
+
const table = require("./discovery-table.cjs");
|
|
39
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
40
|
+
const rollup = require("./discovery-rollup.cjs");
|
|
41
|
+
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
42
|
+
const DISCOVERY_DIR = '.planning/discovery';
|
|
43
|
+
const STORIES_FILE = `${DISCOVERY_DIR}/_stories.md`;
|
|
44
|
+
const REQUIREMENTS_FILE = `${DISCOVERY_DIR}/_requirements.md`;
|
|
45
|
+
const PROCESSES_FILE = `${DISCOVERY_DIR}/_processes.md`;
|
|
46
|
+
const STORIES_SECTION = 'Stories';
|
|
47
|
+
const STORIES_HEADER = Object.freeze(['ID', 'EPIC', 'Requirements', 'Story', 'Acceptance criteria', 'Source step']);
|
|
48
|
+
const US_RE = /^US-(\d{2})$/;
|
|
49
|
+
const EPIC_RE = /^E-\d{2}$/;
|
|
50
|
+
const REQUIREMENT_ID_RE = /^[A-Z][A-Z0-9]*-\d+$/;
|
|
51
|
+
/** `MP-nn #<cell>` — the main process and the step matrix's `#` cell, copied verbatim (`01.3`, `5`). */
|
|
52
|
+
const SOURCE_STEP_RE = /^(MP-\d{2}) #(\S+)$/;
|
|
53
|
+
const MP_HEADING_RE = /^(MP-\d{2})\s+—\s+\S/;
|
|
54
|
+
const SYSTEM_ACTOR = 'System';
|
|
55
|
+
/** An acceptance-criteria cell that only points elsewhere is no criterion the verifier can run. */
|
|
56
|
+
const POINTER_AC_RE = /^(as in|see|zie|idem)\b/i;
|
|
57
|
+
const PENDING = '—';
|
|
58
|
+
/** Requirement prefixes that need no user story: non-functional and technical rows have no actor. */
|
|
59
|
+
const STORYLESS_PREFIXES = Object.freeze(['N-', 'T-']);
|
|
60
|
+
const USAGE = 'Usage: pp-tools discovery stories-check';
|
|
61
|
+
function readIfPresent(abs) {
|
|
62
|
+
if (!node_fs_1.default.existsSync(abs))
|
|
63
|
+
return { kind: 'absent' };
|
|
64
|
+
try {
|
|
65
|
+
return { kind: 'ok', content: node_fs_1.default.readFileSync(abs, 'utf-8') };
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return { kind: 'unreadable' };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function sameHeader(actual, expected) {
|
|
72
|
+
return actual.length === expected.length && actual.every((c, i) => c.trim().toLowerCase() === expected[i].toLowerCase());
|
|
73
|
+
}
|
|
74
|
+
function splitList(cell) {
|
|
75
|
+
return cell.split(',').map((t) => t.trim()).filter(Boolean);
|
|
76
|
+
}
|
|
77
|
+
/** First-column IDs of the first table under a `## <heading>` of another artifact. */
|
|
78
|
+
function firstColumnOf(content, heading) {
|
|
79
|
+
const section = table.splitSections(content.split(/\r?\n/), 2).find((s) => s.heading === heading);
|
|
80
|
+
if (!section)
|
|
81
|
+
return new Set();
|
|
82
|
+
const tables = table.parseTables(section.body);
|
|
83
|
+
if (tables.length === 0)
|
|
84
|
+
return new Set();
|
|
85
|
+
return new Set(tables[0].rows.map((r) => (r.cells[0] ?? '').trim()).filter(Boolean));
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Every step row of `_processes.md` § Steps: its key (`MP-nn #<cell>`), its
|
|
89
|
+
* actors and the requirements it cites. `null` when the section is absent —
|
|
90
|
+
* the step rule then cannot run and only the prefix exemption applies.
|
|
91
|
+
*/
|
|
92
|
+
function stepsOf(content) {
|
|
93
|
+
const steps = table.splitSections(content.split(/\r?\n/), 2).find((s) => s.heading === 'Steps');
|
|
94
|
+
if (!steps)
|
|
95
|
+
return null;
|
|
96
|
+
const out = [];
|
|
97
|
+
for (const sec of table.splitSections(steps.body.map((l) => l.text), 3)) {
|
|
98
|
+
const m = MP_HEADING_RE.exec(sec.heading);
|
|
99
|
+
const tbl = table.parseTables(sec.body)[0];
|
|
100
|
+
if (!m || !tbl)
|
|
101
|
+
continue;
|
|
102
|
+
for (const r of tbl.rows) {
|
|
103
|
+
out.push({ key: `${m[1]} #${(r.cells[0] ?? '').trim()}`, actors: splitList(r.cells[2] ?? ''), requirements: splitList(r.cells[7] ?? '') });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return out;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Does this MVP requirement need a user story? Step 5E derives stories from the
|
|
110
|
+
* step rows where the actor is a persona, so the check mirrors that: a
|
|
111
|
+
* requirement needs one when at least one step citing it has an actor other
|
|
112
|
+
* than `System`. Without a step matrix the prefix exemption is all there is.
|
|
113
|
+
*/
|
|
114
|
+
function needsStory(r, steps) {
|
|
115
|
+
if (STORYLESS_PREFIXES.includes(r.prefix))
|
|
116
|
+
return false;
|
|
117
|
+
if (steps === null)
|
|
118
|
+
return true;
|
|
119
|
+
return steps.some((s) => s.requirements.includes(r.id) && s.actors.some((a) => a !== SYSTEM_ACTOR));
|
|
120
|
+
}
|
|
121
|
+
// ─── Check ────────────────────────────────────────────────────────────────────
|
|
122
|
+
function checkStories(content, sources) {
|
|
123
|
+
const violations = [];
|
|
124
|
+
const sections = table.splitSections(content.split(/\r?\n/), 2);
|
|
125
|
+
const section = sections.find((s) => s.heading === STORIES_SECTION);
|
|
126
|
+
if (!section) {
|
|
127
|
+
return { ok: false, stories: 0, violations: [{ artifact: STORIES_FILE, reason: 'missing_section', id: STORIES_SECTION }] };
|
|
128
|
+
}
|
|
129
|
+
const tables = table.parseTables(section.body);
|
|
130
|
+
if (tables.length === 0) {
|
|
131
|
+
return { ok: false, stories: 0, violations: [{ artifact: STORIES_FILE, reason: 'missing_section', line: section.line, id: STORIES_SECTION }] };
|
|
132
|
+
}
|
|
133
|
+
if (!sameHeader(tables[0].header, STORIES_HEADER)) {
|
|
134
|
+
return { ok: false, stories: 0, violations: [{ artifact: STORIES_FILE, reason: 'bad_table_shape', line: tables[0].line, id: STORIES_SECTION }] };
|
|
135
|
+
}
|
|
136
|
+
const knownIds = sources.requirements instanceof Array ? new Set(sources.requirements.map((r) => r.id)) : null;
|
|
137
|
+
const ids = new Set();
|
|
138
|
+
const seqs = [];
|
|
139
|
+
const storied = new Set();
|
|
140
|
+
for (const row of tables[0].rows) {
|
|
141
|
+
const [id, epic, requirements, story, acceptance, sourceStep] = row.cells.map((c) => c.trim());
|
|
142
|
+
const m = US_RE.exec(id ?? '');
|
|
143
|
+
if (!m) {
|
|
144
|
+
violations.push({ artifact: STORIES_FILE, reason: 'bad_id', line: row.line });
|
|
145
|
+
}
|
|
146
|
+
else if (ids.has(id)) {
|
|
147
|
+
violations.push({ artifact: STORIES_FILE, reason: 'duplicate_id', line: row.line, id });
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
ids.add(id);
|
|
151
|
+
seqs.push(Number(m[1]));
|
|
152
|
+
}
|
|
153
|
+
if (!EPIC_RE.test(epic ?? ''))
|
|
154
|
+
violations.push({ artifact: STORIES_FILE, reason: 'bad_epic', line: row.line });
|
|
155
|
+
if ((story ?? '') === '')
|
|
156
|
+
violations.push({ artifact: STORIES_FILE, reason: 'empty_cell', line: row.line, id: 'Story' });
|
|
157
|
+
if ((acceptance ?? '') === '')
|
|
158
|
+
violations.push({ artifact: STORIES_FILE, reason: 'empty_cell', line: row.line, id: 'Acceptance criteria' });
|
|
159
|
+
else if (POINTER_AC_RE.test(acceptance))
|
|
160
|
+
violations.push({ artifact: STORIES_FILE, reason: 'acceptance_is_pointer', line: row.line, id });
|
|
161
|
+
const reqs = splitList(requirements ?? '');
|
|
162
|
+
if (reqs.length === 0)
|
|
163
|
+
violations.push({ artifact: STORIES_FILE, reason: 'no_requirement', line: row.line });
|
|
164
|
+
for (const r of reqs) {
|
|
165
|
+
if (!REQUIREMENT_ID_RE.test(r))
|
|
166
|
+
violations.push({ artifact: STORIES_FILE, reason: 'unknown_requirement_id', line: row.line, id: r.slice(0, 40) });
|
|
167
|
+
else if (knownIds !== null && !knownIds.has(r))
|
|
168
|
+
violations.push({ artifact: STORIES_FILE, reason: 'unknown_requirement_id', line: row.line, id: r });
|
|
169
|
+
else
|
|
170
|
+
storied.add(r);
|
|
171
|
+
}
|
|
172
|
+
const src = sourceStep ?? '';
|
|
173
|
+
if (src !== PENDING) {
|
|
174
|
+
const refs = splitList(src);
|
|
175
|
+
if (refs.length === 0)
|
|
176
|
+
violations.push({ artifact: STORIES_FILE, reason: 'bad_source_step', line: row.line });
|
|
177
|
+
for (const ref of refs) {
|
|
178
|
+
const sm = SOURCE_STEP_RE.exec(ref);
|
|
179
|
+
if (!sm)
|
|
180
|
+
violations.push({ artifact: STORIES_FILE, reason: 'bad_source_step', line: row.line });
|
|
181
|
+
else if (sources.processes !== null && !sources.processes.has(sm[1]))
|
|
182
|
+
violations.push({ artifact: STORIES_FILE, reason: 'unknown_process_id', line: row.line, id: sm[1] });
|
|
183
|
+
else if (sources.steps !== null && !sources.steps.some((st) => st.key === ref))
|
|
184
|
+
violations.push({ artifact: STORIES_FILE, reason: 'unknown_process_step', line: row.line, id: ref.slice(0, 40) });
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const max = table.boundedSequenceMax(seqs);
|
|
189
|
+
if (max !== null) {
|
|
190
|
+
const have = new Set(seqs);
|
|
191
|
+
for (let n = 1; n <= max; n += 1) {
|
|
192
|
+
if (!have.has(n))
|
|
193
|
+
violations.push({ artifact: STORIES_FILE, reason: 'sequence_gap', id: `US-${String(n).padStart(2, '0')}` });
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (sources.requirements instanceof Array) {
|
|
197
|
+
for (const r of sources.requirements) {
|
|
198
|
+
if (r.disposition !== rollup.DISPOSITIONS.MVP)
|
|
199
|
+
continue;
|
|
200
|
+
if (!needsStory(r, sources.steps))
|
|
201
|
+
continue;
|
|
202
|
+
if (!storied.has(r.id))
|
|
203
|
+
violations.push({ artifact: REQUIREMENTS_FILE, reason: 'mvp_requirement_without_story', id: r.id });
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
else if (sources.requirements === 'unparseable') {
|
|
207
|
+
violations.push({ artifact: REQUIREMENTS_FILE, reason: 'requirements_unparseable' });
|
|
208
|
+
}
|
|
209
|
+
return { ok: violations.length === 0, stories: ids.size, violations };
|
|
210
|
+
}
|
|
211
|
+
function checkProjectStories(cwd) {
|
|
212
|
+
const read = readIfPresent(node_path_1.default.join(cwd, STORIES_FILE));
|
|
213
|
+
if (read.kind === 'absent')
|
|
214
|
+
return null;
|
|
215
|
+
if (read.kind === 'unreadable')
|
|
216
|
+
return { ok: false, file: STORIES_FILE, stories: 0, violations: [{ artifact: STORIES_FILE, reason: 'artifact_unreadable' }] };
|
|
217
|
+
const extra = [];
|
|
218
|
+
const sources = { requirements: null, processes: null, steps: null };
|
|
219
|
+
const req = readIfPresent(node_path_1.default.join(cwd, REQUIREMENTS_FILE));
|
|
220
|
+
if (req.kind === 'unreadable')
|
|
221
|
+
extra.push({ artifact: REQUIREMENTS_FILE, reason: 'artifact_unreadable' });
|
|
222
|
+
if (req.kind === 'ok') {
|
|
223
|
+
const parsed = rollup.parseRequirementsArtifact(req.content);
|
|
224
|
+
sources.requirements = parsed.ok ? parsed.rows.map((r) => ({ id: r.id, prefix: r.prefix, disposition: r.disposition })) : 'unparseable';
|
|
225
|
+
}
|
|
226
|
+
const proc = readIfPresent(node_path_1.default.join(cwd, PROCESSES_FILE));
|
|
227
|
+
if (proc.kind === 'unreadable')
|
|
228
|
+
extra.push({ artifact: PROCESSES_FILE, reason: 'artifact_unreadable' });
|
|
229
|
+
if (proc.kind === 'ok') {
|
|
230
|
+
sources.processes = firstColumnOf(proc.content, 'To-be');
|
|
231
|
+
sources.steps = stepsOf(proc.content);
|
|
232
|
+
}
|
|
233
|
+
const result = checkStories(read.content, sources);
|
|
234
|
+
result.violations.push(...extra);
|
|
235
|
+
result.ok = result.violations.length === 0;
|
|
236
|
+
return { file: STORIES_FILE, ...result };
|
|
237
|
+
}
|
|
238
|
+
function formatViolation(v) {
|
|
239
|
+
const where = v.line !== undefined ? `:${v.line}` : '';
|
|
240
|
+
const what = v.id !== undefined ? ` (${v.id})` : '';
|
|
241
|
+
return ` - ${v.artifact}${where}: ${v.reason}${what}`;
|
|
242
|
+
}
|
|
243
|
+
// ─── CLI adapter ──────────────────────────────────────────────────────────────
|
|
244
|
+
/** `pp-tools discovery stories-check` (dotted `query discovery.stories-check`). Absent artifact → exit 0, `present: false`. */
|
|
245
|
+
function cmdDiscoveryStoriesCheck(cwd, args, raw) {
|
|
246
|
+
if (args.length > 2) {
|
|
247
|
+
error(USAGE, ERROR_REASON.USAGE);
|
|
248
|
+
return; // unreachable
|
|
249
|
+
}
|
|
250
|
+
const result = checkProjectStories(cwd);
|
|
251
|
+
if (result === null) {
|
|
252
|
+
output({ ok: true, present: false, file: STORIES_FILE }, raw);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
const payload = { ok: result.ok, present: true, file: result.file, stories: result.stories };
|
|
256
|
+
if (!result.ok) {
|
|
257
|
+
output({ ...payload, violations: result.violations.length }, raw);
|
|
258
|
+
error(`discovery stories-check: ${result.violations.length} violation(s):\n` + result.violations.map(formatViolation).join('\n'), ERROR_REASON.DISCOVERY_STORIES_CHECK_FAILED);
|
|
259
|
+
return; // unreachable
|
|
260
|
+
}
|
|
261
|
+
output(payload, raw);
|
|
262
|
+
}
|
|
263
|
+
module.exports = {
|
|
264
|
+
STORIES_FILE,
|
|
265
|
+
STORIES_HEADER,
|
|
266
|
+
STORYLESS_PREFIXES,
|
|
267
|
+
SYSTEM_ACTOR,
|
|
268
|
+
needsStory,
|
|
269
|
+
STORIES_CHECK_FAILED: ERROR_REASON.DISCOVERY_STORIES_CHECK_FAILED,
|
|
270
|
+
checkStories,
|
|
271
|
+
checkProjectStories,
|
|
272
|
+
cmdDiscoveryStoriesCheck,
|
|
273
|
+
};
|
|
@@ -40,7 +40,7 @@ const VOID_TAGS = new Set([
|
|
|
40
40
|
const RAW_TEXT_TAGS = new Set(['script', 'style', 'noscript']);
|
|
41
41
|
const ID_RE = /^[A-Za-z][A-Za-z0-9_:.-]{0,79}$/;
|
|
42
42
|
const REQ_ID_RE = /^[A-Z][A-Z0-9]*-\d+$/;
|
|
43
|
-
const DECISION_RE = /^D
|
|
43
|
+
const DECISION_RE = /^D-(?:\d{2}|P\d{2})-\d+$/;
|
|
44
44
|
const ELEMENT_RE = /^(\*|[A-Za-z0-9_.-]+\.html?)#([A-Za-z][A-Za-z0-9_:.-]{0,79})$/;
|
|
45
45
|
const ROUND_RE = /^## Sweep (\d{1,2}) — \d{4}-\d{2}-\d{2} — .+$/;
|
|
46
46
|
const TABLE_HEADER = ['Element', 'Outcome', 'Reference'];
|
|
@@ -63,12 +63,21 @@ function parseAttrs(src) {
|
|
|
63
63
|
}
|
|
64
64
|
return attrs;
|
|
65
65
|
}
|
|
66
|
-
|
|
66
|
+
/**
|
|
67
|
+
* A multi-word PascalCase name (`UiButton`, `UiSelect`): the component spelling
|
|
68
|
+
* of a kebab-case custom element (`ui-button`), which a string template keeps
|
|
69
|
+
* and lowercasing would hide (issue #12, item 8). A single word (`Div`) or an
|
|
70
|
+
* all-caps tag (`BUTTON`) is plain HTML.
|
|
71
|
+
*/
|
|
72
|
+
function isPascalComponent(rawTag) {
|
|
73
|
+
return /^[A-Z][a-z0-9]+[A-Z]/.test(rawTag);
|
|
74
|
+
}
|
|
75
|
+
function isFunctional(tag, attrs, rawTag = tag) {
|
|
67
76
|
if (attrs.has('data-req'))
|
|
68
77
|
return true;
|
|
69
78
|
if (tag === 'input' && (attrs.get('type') || '').toLowerCase() === 'hidden')
|
|
70
79
|
return false;
|
|
71
|
-
if (FUNCTIONAL_TAGS.has(tag) || tag.includes('-'))
|
|
80
|
+
if (FUNCTIONAL_TAGS.has(tag) || tag.includes('-') || isPascalComponent(rawTag))
|
|
72
81
|
return true;
|
|
73
82
|
const role = (attrs.get('role') || '').toLowerCase();
|
|
74
83
|
return FUNCTIONAL_ROLES.has(role);
|
|
@@ -107,7 +116,7 @@ function scanPage(page, html, violations) {
|
|
|
107
116
|
const parentScreen = stack.length ? stack[stack.length - 1].screen : null;
|
|
108
117
|
const screen = screenFrom(tag, attrs) ?? parentScreen;
|
|
109
118
|
const line = lineAt(text, m.index);
|
|
110
|
-
if (isFunctional(tag, attrs)) {
|
|
119
|
+
if (isFunctional(tag, attrs, m[2])) {
|
|
111
120
|
const id = attrs.get('id') ?? null;
|
|
112
121
|
let reqs = [];
|
|
113
122
|
if (attrs.has('data-req')) {
|
|
@@ -90,9 +90,64 @@ function boundedSequenceMax(seqs) {
|
|
|
90
90
|
const max = seqs.reduce((a, b) => (b > a ? b : a), 0);
|
|
91
91
|
return max > MAX_SEQUENCE_SPAN ? null : max;
|
|
92
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* Every pipe table in a run of numbered lines (Discovery Sprint improvement
|
|
95
|
+
* plan, Batch 2). The header is the row before the first separator; data rows
|
|
96
|
+
* follow until the first line that is not a pipe row; a second separator
|
|
97
|
+
* starts the next table. Cells go through `splitTableCells`, so an escaped pipe
|
|
98
|
+
* inside a free-text cell never shifts a column.
|
|
99
|
+
*/
|
|
100
|
+
function parseTables(body) {
|
|
101
|
+
const tables = [];
|
|
102
|
+
for (let i = 1; i < body.length; i += 1) {
|
|
103
|
+
if (!SEPARATOR_ROW_RE.test(body[i].text) || !body[i - 1].text.trim().startsWith('|'))
|
|
104
|
+
continue;
|
|
105
|
+
const table = { line: body[i - 1].n, header: splitTableCells(body[i - 1].text), rows: [] };
|
|
106
|
+
let j = i + 1;
|
|
107
|
+
for (; j < body.length; j += 1) {
|
|
108
|
+
const text = body[j].text.trim();
|
|
109
|
+
if (!text.startsWith('|') || SEPARATOR_ROW_RE.test(text))
|
|
110
|
+
break;
|
|
111
|
+
table.rows.push({ line: body[j].n, cells: splitTableCells(text) });
|
|
112
|
+
}
|
|
113
|
+
tables.push(table);
|
|
114
|
+
i = j - 1;
|
|
115
|
+
}
|
|
116
|
+
return tables;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Split a file into the sections opened by headings of exactly `level` hashes
|
|
120
|
+
* (`## ` for 2). A section's body runs to the next heading of the same or a
|
|
121
|
+
* higher level; lower-level headings stay inside the body, so a caller can
|
|
122
|
+
* split a body again at the next level. Lines before the first heading belong
|
|
123
|
+
* to no section.
|
|
124
|
+
*/
|
|
125
|
+
function splitSections(lines, level) {
|
|
126
|
+
const open = new RegExp(`^#{${level}}\\s+(.*)$`);
|
|
127
|
+
const close = new RegExp(`^#{1,${level}}\\s`);
|
|
128
|
+
const out = [];
|
|
129
|
+
let current = null;
|
|
130
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
131
|
+
const m = open.exec(lines[i]);
|
|
132
|
+
if (m) {
|
|
133
|
+
current = { heading: m[1].trim(), line: i + 1, body: [] };
|
|
134
|
+
out.push(current);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (close.test(lines[i])) {
|
|
138
|
+
current = null;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (current)
|
|
142
|
+
current.body.push({ n: i + 1, text: lines[i] });
|
|
143
|
+
}
|
|
144
|
+
return out;
|
|
145
|
+
}
|
|
93
146
|
module.exports = {
|
|
94
147
|
splitTableCells,
|
|
95
148
|
SEPARATOR_ROW_RE,
|
|
96
149
|
MAX_SEQUENCE_SPAN,
|
|
97
150
|
boundedSequenceMax,
|
|
151
|
+
parseTables,
|
|
152
|
+
splitSections,
|
|
98
153
|
};
|
|
@@ -12,7 +12,11 @@
|
|
|
12
12
|
* - every entry is one well-formed line (`- `<path>` — <why>`, or
|
|
13
13
|
* `- (none) — <why>` when nothing was selected);
|
|
14
14
|
* - every selected document was USED: cited `[vault: <path>]` somewhere in
|
|
15
|
-
* the session notes, or marked `— not used: <reason>` on its own line
|
|
15
|
+
* the session notes, or marked `— not used: <reason>` on its own line;
|
|
16
|
+
* - when the selection used `INDEX.md`, every entry the vault marks
|
|
17
|
+
* `[cross-cutting]` has a record line (issue #12, items 16-17): those rules
|
|
18
|
+
* bind every session, so leaving one out is how an approval route went
|
|
19
|
+
* unseen until Session 12.
|
|
16
20
|
*
|
|
17
21
|
* The selection itself is the facilitator's judgement and is not re-derived
|
|
18
22
|
* here; what is checked is that the record exists, is complete, and that what
|
|
@@ -109,7 +113,7 @@ function parseEntry(n, text) {
|
|
|
109
113
|
* A citation counts only OUTSIDE the section: the record naming a path is not
|
|
110
114
|
* evidence that the path was used.
|
|
111
115
|
*/
|
|
112
|
-
function checkVaultContext(content) {
|
|
116
|
+
function checkVaultContext(content, crossCutting = []) {
|
|
113
117
|
const violations = [];
|
|
114
118
|
const section = extractSection(content);
|
|
115
119
|
if (section === null) {
|
|
@@ -163,8 +167,38 @@ function checkVaultContext(content) {
|
|
|
163
167
|
violations.push({ reason: 'document_unused', line: doc.line, path: doc.path });
|
|
164
168
|
}
|
|
165
169
|
}
|
|
170
|
+
// The cross-cutting entries are owed a line only when the selection read the
|
|
171
|
+
// vault's own index: SUMMARY.md carries no mark, and `none` read nothing.
|
|
172
|
+
if (index === 'INDEX.md') {
|
|
173
|
+
const recorded = new Set(docs.map((d) => d.path));
|
|
174
|
+
for (const p of crossCutting) {
|
|
175
|
+
if (!recorded.has(p))
|
|
176
|
+
violations.push({ reason: 'cross_cutting_missing', path: p });
|
|
177
|
+
}
|
|
178
|
+
}
|
|
166
179
|
return { ok: violations.length === 0, index, documents: docs.length, cited, not_used: notUsed, violations };
|
|
167
180
|
}
|
|
181
|
+
/**
|
|
182
|
+
* The paths the cached `INDEX.md` marks `[cross-cutting]`, read through the
|
|
183
|
+
* vault's one index-state function so the bounded read, the withheld filter
|
|
184
|
+
* and the path rule are the engine's own. Withheld entries never reach it;
|
|
185
|
+
* unsafe ones are dropped, since no record line could name them. A vault that
|
|
186
|
+
* is off, uncached or unreadable owes nothing here — `vault status` reports
|
|
187
|
+
* that state.
|
|
188
|
+
*/
|
|
189
|
+
function crossCuttingPaths(cwd) {
|
|
190
|
+
try {
|
|
191
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
192
|
+
const vault = require('./vault.cjs');
|
|
193
|
+
const state = vault.computeVaultIndexState(cwd);
|
|
194
|
+
if (state === null)
|
|
195
|
+
return [];
|
|
196
|
+
return state.entries.filter((e) => e.crossCutting === true && e.resolution !== 'unsafe').map((e) => e.path);
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
return [];
|
|
200
|
+
}
|
|
201
|
+
}
|
|
168
202
|
/** Parse the session argument: a bare integer 1–13, leading zeros allowed (`03` is session 3). */
|
|
169
203
|
function parseSessionArg(raw) {
|
|
170
204
|
if (typeof raw !== 'string' || !/^\d{1,3}$/.test(raw))
|
|
@@ -189,7 +223,7 @@ function checkSessionVaultContext(cwd, n) {
|
|
|
189
223
|
violations: [{ reason: 'session_file_missing' }],
|
|
190
224
|
};
|
|
191
225
|
}
|
|
192
|
-
return { session: n, file: rel, ...checkVaultContext(content) };
|
|
226
|
+
return { session: n, file: rel, ...checkVaultContext(content, crossCuttingPaths(cwd)) };
|
|
193
227
|
}
|
|
194
228
|
function formatViolation(v) {
|
|
195
229
|
const where = v.line !== undefined ? `line ${v.line}: ` : '';
|
|
@@ -225,7 +225,7 @@ function isVaultEnabledInConfig(parsed) {
|
|
|
225
225
|
// ─── buildInitQuestions ────────────────────────────────────────────────────
|
|
226
226
|
/** The canonical /pp-init question set, as data; the session may answer some itself (see "Automatic answers").
|
|
227
227
|
* No org-profile question in this fork (PBI 74316); provider options come from `PROVIDERS`, preselected via `resolveProvider`. */
|
|
228
|
-
function buildInitQuestions(cwd) {
|
|
228
|
+
function buildInitQuestions(cwd, opts = {}) {
|
|
229
229
|
let preselected = git_provider_cjs_1.DEFAULT_PROVIDER;
|
|
230
230
|
try {
|
|
231
231
|
preselected = (0, git_provider_cjs_1.resolveProvider)(cwd).provider;
|
|
@@ -247,6 +247,7 @@ function buildInitQuestions(cwd) {
|
|
|
247
247
|
prompt: 'Client slug (kebab-case, e.g. "acme") — type "internal" if none',
|
|
248
248
|
optionalAnswer: false,
|
|
249
249
|
},
|
|
250
|
+
...clientVaultQuestion(cwd, opts),
|
|
250
251
|
{ id: 'description', prompt: 'One-line description (what this project does)', optionalAnswer: false },
|
|
251
252
|
{
|
|
252
253
|
id: 'provider',
|
|
@@ -259,6 +260,23 @@ function buildInitQuestions(cwd) {
|
|
|
259
260
|
{ id: 'visibility', prompt: 'Visibility (private / public / internal)', optionalAnswer: false },
|
|
260
261
|
];
|
|
261
262
|
}
|
|
263
|
+
/** The client-vault question, asked while no vault is bound and a binding can be composed (`clientVaultChoice`).
|
|
264
|
+
* Blank or "none" keeps the org profile's seeded vault, if any; an answer replaces it in the binding.
|
|
265
|
+
* An internal project is answered "none" automatically (see `autoAnswerFor`). */
|
|
266
|
+
function clientVaultQuestion(cwd, opts) {
|
|
267
|
+
const choice = (0, compose_bindings_cjs_1.clientVaultChoice)(cwd, opts);
|
|
268
|
+
if (!choice.offered)
|
|
269
|
+
return [];
|
|
270
|
+
return [
|
|
271
|
+
{
|
|
272
|
+
id: 'client_vault',
|
|
273
|
+
prompt: choice.defaultRepo === null
|
|
274
|
+
? 'Client vault repo ("owner/name" or a full clone URL) — type "none" to bind no client vault'
|
|
275
|
+
: `Client vault repo ("owner/name" or a full clone URL) — type "none" to use the default vault (${choice.defaultRepo})`,
|
|
276
|
+
optionalAnswer: true,
|
|
277
|
+
},
|
|
278
|
+
];
|
|
279
|
+
}
|
|
262
280
|
// ─── Automatic answers (PBI 74299): an org profile init section fixes answers and can derive slugs ───
|
|
263
281
|
/** The visibility answers the question accepts. */
|
|
264
282
|
const VISIBILITY_OPTIONS = Object.freeze(['private', 'public', 'internal']);
|
|
@@ -320,6 +338,9 @@ function autoAnswerFor(id, answers, init) {
|
|
|
320
338
|
const value = init.fixed[id];
|
|
321
339
|
return value === undefined ? null : { value, source: 'org-profile' };
|
|
322
340
|
}
|
|
341
|
+
if (id === 'client_vault' && answers.client_slug === 'internal') {
|
|
342
|
+
return { value: 'none', source: 'derived' };
|
|
343
|
+
}
|
|
323
344
|
if (init.deriveSlugs && (id === 'project_slug' || id === 'client_slug')) {
|
|
324
345
|
const name = answers[id === 'project_slug' ? 'project_name' : 'client_name'];
|
|
325
346
|
const slug = typeof name === 'string' ? deriveSlug(name) : null;
|
|
@@ -328,10 +349,10 @@ function autoAnswerFor(id, answers, init) {
|
|
|
328
349
|
return null;
|
|
329
350
|
}
|
|
330
351
|
/** The session's automatic answers, in question order. */
|
|
331
|
-
function listAutoAnswers(cwd, session) {
|
|
352
|
+
function listAutoAnswers(cwd, session, opts) {
|
|
332
353
|
const sources = session.autoAnswers ?? {};
|
|
333
354
|
const out = [];
|
|
334
|
-
for (const q of buildInitQuestions(cwd)) {
|
|
355
|
+
for (const q of buildInitQuestions(cwd, { _globalRoot: opts._globalRoot })) {
|
|
335
356
|
const source = sources[q.id];
|
|
336
357
|
const value = session.answers[q.id];
|
|
337
358
|
if (source !== undefined && typeof value === 'string')
|
|
@@ -758,10 +779,15 @@ function normalizeYesNo(value) {
|
|
|
758
779
|
}
|
|
759
780
|
/** Decide the current step, performing its idempotent side effects once: automatic answers, the identity write,
|
|
760
781
|
* the compose decision and `doneAt`. Recomputes the question set on every call. */
|
|
782
|
+
/** The client vault the operator named, or `undefined` to keep the seeded one. */
|
|
783
|
+
function answeredClientVault(session) {
|
|
784
|
+
const checked = (0, compose_bindings_cjs_1.validateClientVaultAnswer)(session.answers.client_vault);
|
|
785
|
+
return checked.ok && checked.value !== null ? checked.value : undefined;
|
|
786
|
+
}
|
|
761
787
|
function computeCurrentStep(cwd, session, opts) {
|
|
762
788
|
// Any failure degrades to a `write_failed` step instead of throwing.
|
|
763
789
|
try {
|
|
764
|
-
const questions = buildInitQuestions(cwd);
|
|
790
|
+
const questions = buildInitQuestions(cwd, { _globalRoot: opts._globalRoot });
|
|
765
791
|
// PBI 74299: read once per call; only consulted for an unanswered question.
|
|
766
792
|
let profileInit = null;
|
|
767
793
|
for (const q of questions) {
|
|
@@ -784,7 +810,11 @@ function computeCurrentStep(cwd, session, opts) {
|
|
|
784
810
|
persistSession(cwd, session);
|
|
785
811
|
}
|
|
786
812
|
if (session.composeDecision === null) {
|
|
787
|
-
const dryRun = (0, compose_bindings_cjs_1.composeBindings)(cwd, {
|
|
813
|
+
const dryRun = (0, compose_bindings_cjs_1.composeBindings)(cwd, {
|
|
814
|
+
offline: opts.offline === true,
|
|
815
|
+
_globalRoot: opts._globalRoot,
|
|
816
|
+
clientVaultRepo: answeredClientVault(session),
|
|
817
|
+
});
|
|
788
818
|
// Offer compose_confirm only when approving would change something; 'unchanged' and 'skipped' are distinct end states.
|
|
789
819
|
if (dryRun.composable === true && dryRun.action !== 'unchanged') {
|
|
790
820
|
// VAULT-EGRESS-1: `info` is display-only (the CLI and the TTY renderer print it); approval
|
|
@@ -889,7 +919,7 @@ function initSessionNext(cwd, opts = {}) {
|
|
|
889
919
|
}
|
|
890
920
|
const session = loaded.session;
|
|
891
921
|
const state = currentStepToState(computeCurrentStep(cwd, session, opts));
|
|
892
|
-
const autoAnswered = listAutoAnswers(cwd, session);
|
|
922
|
+
const autoAnswered = listAutoAnswers(cwd, session, opts);
|
|
893
923
|
return autoAnswered.length > 0 ? { ...state, autoAnswered } : state;
|
|
894
924
|
}
|
|
895
925
|
/** Record one answer for the current step only; out-of-order, blank required, unparseable or post-done answers are rejected. */
|
|
@@ -929,6 +959,12 @@ function initSessionAnswer(cwd, stepId, value, opts = {}) {
|
|
|
929
959
|
return { ok: false, reason: 'answer_required', message: `Question "${stepId}" requires a non-blank answer.` };
|
|
930
960
|
}
|
|
931
961
|
const answerValue = typeof value === 'string' ? value : '';
|
|
962
|
+
if (stepId === 'client_vault') {
|
|
963
|
+
const checked = (0, compose_bindings_cjs_1.validateClientVaultAnswer)(answerValue);
|
|
964
|
+
if (!checked.ok) {
|
|
965
|
+
return { ok: false, reason: 'invalid_answer', message: `Question "client_vault": ${checked.reason}` };
|
|
966
|
+
}
|
|
967
|
+
}
|
|
932
968
|
session.answers[stepId] = answerValue;
|
|
933
969
|
persistSession(cwd, session);
|
|
934
970
|
return { ok: true };
|
|
@@ -951,7 +987,12 @@ function initSessionAnswer(cwd, stepId, value, opts = {}) {
|
|
|
951
987
|
}
|
|
952
988
|
if (normalized === true) {
|
|
953
989
|
// Check the write result before recording "approved": the file can change between the dry run and this write.
|
|
954
|
-
const writeResult = (0, compose_bindings_cjs_1.composeBindings)(cwd, {
|
|
990
|
+
const writeResult = (0, compose_bindings_cjs_1.composeBindings)(cwd, {
|
|
991
|
+
write: true,
|
|
992
|
+
offline: opts.offline === true,
|
|
993
|
+
_globalRoot: opts._globalRoot,
|
|
994
|
+
clientVaultRepo: answeredClientVault(session),
|
|
995
|
+
});
|
|
955
996
|
if (writeResult.composable !== true || writeResult.wrote !== true) {
|
|
956
997
|
// Leave the decision unset so the next call re-evaluates and can re-offer compose_confirm.
|
|
957
998
|
return {
|
package/pp-core/bin/lib/io.cjs
CHANGED
|
@@ -203,6 +203,28 @@ const ERROR_REASON = Object.freeze({
|
|
|
203
203
|
DISCOVERY_REBASELINE_CHECK_FAILED: 'discovery_rebaseline_check_failed',
|
|
204
204
|
// discovery vault context record (PBI 75336)
|
|
205
205
|
DISCOVERY_VAULT_CHECK_FAILED: 'discovery_vault_check_failed',
|
|
206
|
+
// discovery project charter (Discovery Sprint improvement plan, Batch 1)
|
|
207
|
+
DISCOVERY_CHARTER_CHECK_FAILED: 'discovery_charter_check_failed',
|
|
208
|
+
DISCOVERY_CHARTER_UNSIGNED: 'discovery_charter_unsigned',
|
|
209
|
+
// discovery data model, processes, business rules (improvement plan, Batch 2)
|
|
210
|
+
DISCOVERY_ENTITY_CHECK_FAILED: 'discovery_entity_check_failed',
|
|
211
|
+
DISCOVERY_ENTITY_CHECK_INCOMPLETE: 'discovery_entity_check_incomplete',
|
|
212
|
+
DISCOVERY_PROCESS_CHECK_FAILED: 'discovery_process_check_failed',
|
|
213
|
+
DISCOVERY_PROCESS_STEPS_UNCITED: 'discovery_process_steps_uncited',
|
|
214
|
+
DISCOVERY_RULES_CHECK_FAILED: 'discovery_rules_check_failed',
|
|
215
|
+
DISCOVERY_STORIES_CHECK_FAILED: 'discovery_stories_check_failed',
|
|
216
|
+
DISCOVERY_PHASES_CHECK_FAILED: 'discovery_phases_check_failed',
|
|
217
|
+
DISCOVERY_FOR_PHASE_FAILED: 'discovery_for_phase_failed',
|
|
218
|
+
// discovery roles, integrations, migration, deviations (improvement plan, Batch 3)
|
|
219
|
+
DISCOVERY_ROLES_CHECK_FAILED: 'discovery_roles_check_failed',
|
|
220
|
+
DISCOVERY_INTEGRATIONS_CHECK_FAILED: 'discovery_integrations_check_failed',
|
|
221
|
+
DISCOVERY_INTEGRATIONS_UNCITED: 'discovery_integrations_uncited',
|
|
222
|
+
DISCOVERY_MIGRATION_CHECK_FAILED: 'discovery_migration_check_failed',
|
|
223
|
+
DISCOVERY_MIGRATION_UNCITED: 'discovery_migration_uncited',
|
|
224
|
+
// discovery session output gate (improvement plan, Batch 5)
|
|
225
|
+
DISCOVERY_SESSION_CHECK_FAILED: 'discovery_session_check_failed',
|
|
226
|
+
DISCOVERY_DEVIATIONS_CHECK_FAILED: 'discovery_deviations_check_failed',
|
|
227
|
+
DISCOVERY_DEVIATIONS_UNAPPROVED: 'discovery_deviations_unapproved',
|
|
206
228
|
// generic
|
|
207
229
|
USAGE: 'usage',
|
|
208
230
|
UNKNOWN: 'unknown',
|
|
@@ -73,7 +73,7 @@ exports.DEFAULT_HOST_FOR_PROVIDER = Object.freeze({
|
|
|
73
73
|
github: 'github.com',
|
|
74
74
|
});
|
|
75
75
|
/** The values moving out of `src/vault.cts`'s `DEFAULT_FALLBACK_REPO`/`DEFAULT_FALLBACK_REF`. */
|
|
76
|
-
exports.DEFAULT_SHARED_REPO = '
|
|
76
|
+
exports.DEFAULT_SHARED_REPO = 'pareto-ooo/pareto-vault-releases';
|
|
77
77
|
exports.DEFAULT_SHARED_REF = 'main';
|
|
78
78
|
/** Dotted `.planning/config.json` keys, named so no consumer hand-writes a key string. */
|
|
79
79
|
exports.SHARED_REPO_CONFIG_KEY = 'vault.fallback_repo';
|