@pareto4/pp-core 1.11.1 → 1.12.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/README.md +31 -0
- package/agents/pp-code-fixer.md +1 -1
- package/agents/pp-debug-session-manager.md +1 -1
- package/agents/pp-debugger.md +1 -1
- package/agents/pp-executor.md +20 -10
- package/agents/pp-intel-updater.md +1 -1
- package/agents/pp-phase-researcher.md +1 -1
- package/agents/pp-plan-checker.md +5 -5
- package/agents/pp-planner.md +10 -24
- package/agents/pp-project-researcher.md +1 -1
- package/agents/pp-research-synthesizer.md +1 -1
- package/agents/pp-ui-researcher.md +1 -1
- package/agents/pp-verifier.md +8 -4
- package/bin/install.js +429 -82
- package/commands/pp-autonomous.md +8 -6
- package/commands/pp-discovery-to-build.md +1 -1
- package/commands/pp-discovery.md +3 -2
- package/commands/pp-discuss-phase.md +2 -2
- package/commands/pp-execute-phase.md +3 -1
- package/commands/pp-graphify.md +5 -5
- package/commands/pp-import.md +1 -1
- package/commands/pp-map-codebase.md +1 -1
- package/commands/pp-ns-project.md +2 -3
- package/commands/pp-ns-workflow.md +1 -1
- package/commands/pp-plan-phase.md +2 -1
- package/commands/pp-verify-work.md +2 -1
- package/gemini-extension.json +1 -1
- package/hooks/dist/pp-cursor-session-start.js +2 -2
- package/hooks/dist/pp-session-state.sh +1 -1
- package/hooks/dist/pp-statusline.js +21 -2
- package/hooks/pp-cursor-session-start.js +2 -2
- package/hooks/pp-session-state.sh +1 -1
- package/hooks/pp-statusline.js +21 -2
- package/package.json +18 -5
- package/pareto-skills/pp-project-setup/SKILL.md +5 -63
- package/pp-core/bin/detect-build-commands.sh +182 -0
- package/pp-core/bin/lib/capability-registry.cjs +65 -7
- package/pp-core/bin/lib/check-command-router.cjs +79 -38
- package/pp-core/bin/lib/clusters.cjs +2 -3
- package/pp-core/bin/lib/command-aliases.cjs +36 -0
- package/pp-core/bin/lib/command-arg-projection.cjs +99 -1
- package/pp-core/bin/lib/commands.cjs +21 -6
- package/pp-core/bin/lib/compose-bindings.cjs +70 -6
- package/pp-core/bin/lib/config-schema.cjs +16 -0
- package/pp-core/bin/lib/config.cjs +142 -27
- package/pp-core/bin/lib/core-utils.cjs +43 -7
- package/pp-core/bin/lib/discovery-input-check.cjs +218 -0
- package/pp-core/bin/lib/discovery-rebaseline.cjs +1372 -0
- package/pp-core/bin/lib/discovery-research-track.cjs +979 -0
- package/pp-core/bin/lib/discovery-rollup.cjs +476 -0
- package/pp-core/bin/lib/discovery-screens.cjs +1995 -0
- package/pp-core/bin/lib/discovery-sweep.cjs +390 -0
- package/pp-core/bin/lib/discovery-table.cjs +98 -0
- package/pp-core/bin/lib/dispatch-plan.cjs +472 -0
- package/pp-core/bin/lib/frontmatter.cjs +17 -2
- package/pp-core/bin/lib/init-command-router.cjs +8 -5
- package/pp-core/bin/lib/init-contract.cjs +93 -4
- package/pp-core/bin/lib/init.cjs +124 -134
- package/pp-core/bin/lib/install-profiles.cjs +5 -0
- package/pp-core/bin/lib/install-run.cjs +536 -0
- package/pp-core/bin/lib/io.cjs +15 -0
- package/pp-core/bin/lib/loop-resolver.cjs +24 -0
- package/pp-core/bin/lib/milestone.cjs +345 -48
- package/pp-core/bin/lib/phase-command-router.cjs +23 -1
- package/pp-core/bin/lib/phase-locator.cjs +15 -6
- package/pp-core/bin/lib/phase-scope.cjs +91 -0
- package/pp-core/bin/lib/phase-tier.cjs +467 -0
- package/pp-core/bin/lib/phase.cjs +247 -21
- package/pp-core/bin/lib/phases-command-router.cjs +2 -0
- package/pp-core/bin/lib/plan-graph.cjs +368 -0
- package/pp-core/bin/lib/planning-workspace.cjs +53 -0
- package/pp-core/bin/lib/platform-bindings.cjs +32 -2
- package/pp-core/bin/lib/profile-output.cjs +11 -3
- package/pp-core/bin/lib/prohibition-enforcement.cjs +112 -0
- package/pp-core/bin/lib/roadmap-parser.cjs +366 -99
- package/pp-core/bin/lib/roadmap.cjs +141 -39
- package/pp-core/bin/lib/runtime-hooks-surface.cjs +1 -1
- package/pp-core/bin/lib/shell-command-projection.cjs +24 -0
- package/pp-core/bin/lib/state-command-router.cjs +82 -13
- package/pp-core/bin/lib/state-document.cjs +124 -7
- package/pp-core/bin/lib/state.cjs +784 -257
- package/pp-core/bin/lib/template.cjs +22 -6
- package/pp-core/bin/lib/vault-command-router.cjs +57 -6
- package/pp-core/bin/lib/vault-practices.cjs +6 -36
- package/pp-core/bin/lib/vault-write-back.cjs +20 -4
- package/pp-core/bin/lib/vault.cjs +555 -23
- package/pp-core/bin/lib/verify.cjs +53 -31
- package/pp-core/bin/lib/workstream.cjs +1 -1
- package/pp-core/bin/lib/worktree-safety.cjs +26 -15
- package/pp-core/bin/portable-timeout.sh +89 -0
- package/pp-core/bin/pp-tools.cjs +149 -9
- package/pp-core/bin/shared/config-defaults.manifest.json +7 -0
- package/pp-core/bin/shared/config-schema.manifest.json +3 -0
- package/pp-core/references/autonomous-best-effort.md +461 -0
- package/pp-core/references/autonomous-lifecycle.md +146 -0
- package/pp-core/references/autonomous-smart-discuss.md +16 -12
- package/pp-core/references/checkpoints.md +1 -1
- package/pp-core/references/discovery-id-conventions.md +202 -0
- package/pp-core/references/discovery-rebaseline.md +254 -0
- package/pp-core/references/discovery-research-track.md +283 -0
- package/pp-core/references/discovery-traceability.md +400 -0
- package/pp-core/references/loop-hook-dispatch.md +7 -0
- package/pp-core/references/mandatory-initial-read.md +1 -1
- package/pp-core/references/planner-gap-closure.md +1 -0
- package/pp-core/references/planner-human-verify-mode.md +1 -1
- package/pp-core/references/planner-load-graph-context.md +1 -1
- package/pp-core/references/planner-plan-graph.md +66 -0
- package/pp-core/references/planner-tdd.md +1 -0
- package/pp-core/references/planning-config.md +8 -0
- package/pp-core/references/sketch-tooling.md +4 -0
- package/pp-core/references/tdd.md +1 -0
- package/pp-core/templates/claude-md.md +1 -1
- package/pp-core/templates/phase-prompt.md +6 -2
- package/pp-core/templates/prototype-review/review.js +303 -0
- package/pp-core/templates/state.md +6 -0
- package/pp-core/workflows/_runtime-launcher.snippet.sh +1 -1
- package/pp-core/workflows/add-backlog.md +1 -1
- package/pp-core/workflows/add-phase.md +1 -1
- package/pp-core/workflows/add-tests.md +1 -1
- package/pp-core/workflows/add-todo.md +1 -1
- package/pp-core/workflows/ai-integration-phase.md +2 -2
- package/pp-core/workflows/audit-fix.md +12 -16
- package/pp-core/workflows/audit-milestone.md +1 -1
- package/pp-core/workflows/audit-uat.md +1 -1
- package/pp-core/workflows/autonomous.md +68 -306
- package/pp-core/workflows/check-todos.md +1 -1
- package/pp-core/workflows/cleanup.md +1 -1
- package/pp-core/workflows/code-review-fix.md +1 -1
- package/pp-core/workflows/code-review.md +19 -7
- package/pp-core/workflows/complete-milestone.md +41 -60
- package/pp-core/workflows/debug.md +1 -1
- package/pp-core/workflows/diagnose-issues.md +1 -1
- package/pp-core/workflows/discovery-sprint/gap-analysis.md +140 -0
- package/pp-core/workflows/discovery-sprint/input-package.md +140 -0
- package/pp-core/workflows/discovery-sprint/prefill.md +92 -0
- package/pp-core/workflows/discovery-sprint/prototype.md +257 -0
- package/pp-core/workflows/discovery-sprint/rebaseline.md +276 -0
- package/pp-core/workflows/discovery-sprint/session-13-gates.md +24 -0
- package/pp-core/workflows/discovery-sprint/sessions/01-kick-off.md +92 -0
- package/pp-core/workflows/discovery-sprint/sessions/02-as-is.md +98 -0
- package/pp-core/workflows/discovery-sprint/sessions/03-to-be.md +215 -0
- package/pp-core/workflows/discovery-sprint/sessions/04-ux-design.md +146 -0
- package/pp-core/workflows/discovery-sprint/sessions/05-architecture.md +98 -0
- package/pp-core/workflows/discovery-sprint/sessions/06-security.md +107 -0
- package/pp-core/workflows/discovery-sprint/sessions/07-identity.md +98 -0
- package/pp-core/workflows/discovery-sprint/sessions/08-integration.md +85 -0
- package/pp-core/workflows/discovery-sprint/sessions/09-qa.md +109 -0
- package/pp-core/workflows/discovery-sprint/sessions/10-infrastructure.md +119 -0
- package/pp-core/workflows/discovery-sprint/sessions/11-roadmap.md +92 -0
- package/pp-core/workflows/discovery-sprint/sessions/12-delivery-model.md +98 -0
- package/pp-core/workflows/discovery-sprint/sessions/13-wrap-up.md +205 -0
- package/pp-core/workflows/discovery-sprint/templates/context-template.md +266 -0
- package/pp-core/workflows/discovery-sprint.md +79 -1714
- package/pp-core/workflows/discovery-to-build/steps/architecture-deliverables.md +5 -5
- package/pp-core/workflows/discovery-to-build/steps/screen-inventory-gate.md +70 -0
- package/pp-core/workflows/discovery-to-build/templates/discovery-references.md +54 -0
- package/pp-core/workflows/discovery-to-build/templates/pp-handoff.md +184 -0
- package/pp-core/workflows/discovery-to-build.md +21 -238
- package/pp-core/workflows/discuss-phase/modes/advisor.md +1 -1
- package/pp-core/workflows/discuss-phase/modes/auto.md +1 -1
- package/pp-core/workflows/discuss-phase/modes/chain.md +1 -1
- package/pp-core/workflows/discuss-phase-assumptions.md +1 -1
- package/pp-core/workflows/discuss-phase.md +9 -2
- package/pp-core/workflows/do.md +4 -4
- package/pp-core/workflows/docs-update.md +1 -1
- package/pp-core/workflows/edit-phase.md +1 -1
- package/pp-core/workflows/eval-review.md +1 -1
- package/pp-core/workflows/execute-phase/steps/checkpoint-handling.md +45 -0
- package/pp-core/workflows/execute-phase/steps/client-context-gate.md +1 -1
- package/pp-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
- package/pp-core/workflows/execute-phase/steps/cross-ai-delegation.md +90 -0
- package/pp-core/workflows/execute-phase/steps/install-run.md +29 -0
- package/pp-core/workflows/execute-phase/steps/phase-close-rebaseline.md +23 -0
- package/pp-core/workflows/execute-phase/steps/post-merge-gate.md +76 -77
- package/pp-core/workflows/execute-phase/steps/serial-group-dispatch.md +72 -0
- package/pp-core/workflows/execute-phase/steps/vault-fetch.md +5 -5
- package/pp-core/workflows/execute-phase/steps/vault-load-gate.md +19 -112
- package/pp-core/workflows/execute-phase.md +102 -216
- package/pp-core/workflows/execute-plan.md +6 -2
- package/pp-core/workflows/explore.md +1 -1
- package/pp-core/workflows/extract-learnings.md +1 -1
- package/pp-core/workflows/forensics.md +4 -4
- package/pp-core/workflows/graduation.md +1 -1
- package/pp-core/workflows/health.md +1 -1
- package/pp-core/workflows/help/modes/brief.md +1 -0
- package/pp-core/workflows/help/modes/default.md +2 -1
- package/pp-core/workflows/help/modes/full.md +12 -5
- package/pp-core/workflows/import.md +1 -1
- package/pp-core/workflows/ingest-docs.md +1 -1
- package/pp-core/workflows/init.md +18 -2
- package/pp-core/workflows/insert-phase.md +1 -1
- package/pp-core/workflows/list-workspaces.md +1 -1
- package/pp-core/workflows/manager.md +3 -3
- package/pp-core/workflows/map-codebase.md +2 -2
- package/pp-core/workflows/milestone-summary.md +4 -4
- package/pp-core/workflows/mvp-phase.md +1 -1
- package/pp-core/workflows/new-milestone.md +7 -8
- package/pp-core/workflows/new-project.md +6 -1
- package/pp-core/workflows/new-workspace.md +1 -1
- package/pp-core/workflows/next.md +2 -2
- package/pp-core/workflows/note.md +2 -2
- package/pp-core/workflows/pause-work.md +1 -1
- package/pp-core/workflows/plan-milestone-gaps.md +1 -1
- package/pp-core/workflows/plan-phase/steps/chunked-planning.md +111 -0
- package/pp-core/workflows/plan-phase/steps/phase-tier.md +34 -0
- package/pp-core/workflows/plan-phase.md +43 -167
- package/pp-core/workflows/plan-review-convergence.md +2 -2
- package/pp-core/workflows/plant-seed.md +1 -1
- package/pp-core/workflows/pr-branch.md +1 -1
- package/pp-core/workflows/profile-user.md +1 -1
- package/pp-core/workflows/progress.md +3 -3
- package/pp-core/workflows/quick.md +3 -1
- package/pp-core/workflows/remove-phase.md +1 -1
- package/pp-core/workflows/remove-workspace.md +1 -1
- package/pp-core/workflows/resume-project.md +6 -12
- package/pp-core/workflows/review.md +1 -1
- package/pp-core/workflows/scan.md +1 -1
- package/pp-core/workflows/secure-phase.md +1 -1
- package/pp-core/workflows/settings-advanced.md +1 -1
- package/pp-core/workflows/settings-integrations.md +1 -1
- package/pp-core/workflows/settings.md +1 -1
- package/pp-core/workflows/ship.md +25 -8
- package/pp-core/workflows/sketch-wrap-up.md +1 -1
- package/pp-core/workflows/sketch.md +2 -1
- package/pp-core/workflows/spec-phase.md +1 -1
- package/pp-core/workflows/spike-wrap-up.md +1 -1
- package/pp-core/workflows/spike.md +1 -1
- package/pp-core/workflows/stats.md +2 -2
- package/pp-core/workflows/thread.md +1 -1
- package/pp-core/workflows/transition.md +5 -9
- package/pp-core/workflows/ui-phase.md +23 -2
- package/pp-core/workflows/ui-review.md +1 -1
- package/pp-core/workflows/ultraplan-phase.md +2 -2
- package/pp-core/workflows/validate-phase.md +1 -1
- package/pp-core/workflows/vault-write-back.md +1 -1
- package/pp-core/workflows/verify-phase.md +29 -26
- package/pp-core/workflows/verify-work.md +14 -10
- package/scripts/base64-scan.sh +7 -41
- package/scripts/changeset/cli.cjs +25 -5
- package/scripts/dev-env.cjs +462 -0
- package/scripts/lib/allowlist-ratchet.cjs +103 -1
- package/scripts/lib/size-budget.cjs +311 -0
- package/scripts/lint-neutrality-names.cjs +694 -0
- package/scripts/lint-org-coordinates.allowlist.json +35 -0
- package/scripts/lint-org-coordinates.cjs +426 -0
- package/scripts/lint-skill-deps.cjs +48 -9
- package/scripts/lint-test-file-count.cjs +17 -2
- package/scripts/prompt-injection-scan.sh +41 -20
- package/scripts/sync-manifest-versions.cjs +41 -7
- package/scripts/sync-runtime-launcher.cjs +106 -1
- package/scripts/update-size-baseline.cjs +274 -30
- package/scripts/workflow-size.cjs +61 -9
- package/commands/pp-build-handoff.md +0 -46
- package/pp-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +0 -94
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Discovery requirements rollup engine (discovery-v2 Phase 1, REQM-04, D-05..D-08).
|
|
4
|
+
*
|
|
5
|
+
* Parses the per-engagement `.planning/discovery/_requirements.md` artifact
|
|
6
|
+
* (grammar single-source: pp-core/references/discovery-id-conventions.md) and
|
|
7
|
+
* computes per-prefix × per-disposition totals with explicit ID enumeration
|
|
8
|
+
* per bucket, plus the per-prefix next mintable ID (D-12: derived, never stored).
|
|
9
|
+
*
|
|
10
|
+
* Phase 2 (REQM-05, D-26/D-27): validates the Provenance cell — origin
|
|
11
|
+
* (accession cite / session ref / decision ID) plus an optional closed-verb
|
|
12
|
+
* relation suffix (`refines` / `absorbed into`) whose target requirement ID
|
|
13
|
+
* must exist in the same table. Grammar single-source: same reference file.
|
|
14
|
+
*
|
|
15
|
+
* Phase 3 (D-44), extended by Phase 4 (D-50): a Prefix Registry row may not
|
|
16
|
+
* claim one of the six reserved prefixes (`D-` `A-` `SP-` `DG-` `RS-` `SCR-`)
|
|
17
|
+
* — they all match the requirement-ID regex, so registering one collides with
|
|
18
|
+
* the decision / action / sparring / data-gathering / research / screen ID
|
|
19
|
+
* families. Reserved set single-source: same reference file.
|
|
20
|
+
*
|
|
21
|
+
* Fail-loud contract (D-08): any unparseable row, malformed/duplicate ID,
|
|
22
|
+
* unknown prefix, out-of-enum MoSCoW/Disposition, or strike/disposition
|
|
23
|
+
* inconsistency is a structured error naming the offending row. There is no
|
|
24
|
+
* warn-and-skip — a row the parser cannot count is drift by definition.
|
|
25
|
+
*
|
|
26
|
+
* The command is read-only (D-06): it emits JSON plus a rendered markdown
|
|
27
|
+
* totals table; the workflow replaces the `<!-- rollup:auto -->` block with
|
|
28
|
+
* that output verbatim. `--check` (D-07) recomputes and exits non-zero when
|
|
29
|
+
* the written block disagrees with the rows.
|
|
30
|
+
*/
|
|
31
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
32
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
33
|
+
};
|
|
34
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
35
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
36
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
37
|
+
const io = require("./io.cjs");
|
|
38
|
+
const { output, error, ERROR_REASON } = io;
|
|
39
|
+
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
40
|
+
const DISPOSITIONS = Object.freeze({ MVP: 'MVP', LATER: 'Later', DROPPED: 'Dropped' });
|
|
41
|
+
const MOSCOW = Object.freeze({ MUST: 'Must', SHOULD: 'Should', COULD: 'Could', WONT: 'Wont' });
|
|
42
|
+
// D-44 (discovery-v2 Phase 3): these six all match the requirement-ID regex, so
|
|
43
|
+
// registering one as a domain prefix collides with the decision/action/research
|
|
44
|
+
// ID families. See references/discovery-id-conventions.md.
|
|
45
|
+
// D-50 (discovery-v2 Phase 4) added `SCR-`: screen IDs are a flat global
|
|
46
|
+
// `SCR-<nn>` sequence, so an engagement registering `SCR-` as a domain prefix
|
|
47
|
+
// would make `SCR-01` ambiguous between the screen inventory and the
|
|
48
|
+
// requirements table.
|
|
49
|
+
const RESERVED_PREFIXES = new Set(['D-', 'A-', 'SP-', 'DG-', 'RS-', 'SCR-']);
|
|
50
|
+
const ROW_ID_RE = /^([A-Z][A-Z0-9]*-)(\d+)$/;
|
|
51
|
+
const DROPPED_RE = /^Dropped \((D-\d{2}-\d+)\)$/;
|
|
52
|
+
const SEPARATOR_ROW_RE = /^\s*\|[\s:|-]+\|\s*$/;
|
|
53
|
+
const ROLLUP_OPEN = '<!-- rollup:auto -->';
|
|
54
|
+
const ROLLUP_CLOSE = '<!-- /rollup:auto -->';
|
|
55
|
+
const REGISTRY_PREFIX_RE = /^[A-Z][A-Z0-9]*-$/;
|
|
56
|
+
const STRIKE_RE = /^~~.+~~$/;
|
|
57
|
+
// Provenance cell grammar (REQM-05, D-26/D-27) — anchored, non-backtracking,
|
|
58
|
+
// applied to already-split single cells (T-01-02 idiom carried forward; the
|
|
59
|
+
// accession locator is bounded to 80 chars, T-02-02). Separator is
|
|
60
|
+
// space + middle dot U+00B7 + space.
|
|
61
|
+
const PROVENANCE_SEP = ' · ';
|
|
62
|
+
const ACCESSION_CITE_RE = /^\[(\d{2})( §[^\]]{1,80})?\]$/;
|
|
63
|
+
const SESSION_REF_RE = /^S\d+$/;
|
|
64
|
+
const PROVENANCE_DECISION_RE = /^D-\d{2}-\d+$/;
|
|
65
|
+
const RELATION_RE = /^(refines|absorbed into) ([A-Z][A-Z0-9]*-\d+)$/;
|
|
66
|
+
const MOSCOW_SET = new Set(Object.values(MOSCOW));
|
|
67
|
+
const REGISTRY_HEADING = '## Prefix Registry';
|
|
68
|
+
const REQUIREMENTS_HEADING = '## Requirements';
|
|
69
|
+
// ─── Parsing ──────────────────────────────────────────────────────────────────
|
|
70
|
+
/** Split a pipe-table line into trimmed cell strings (outer pipes stripped). */
|
|
71
|
+
function splitCells(line) {
|
|
72
|
+
const trimmed = line.trim();
|
|
73
|
+
const inner = trimmed.replace(/^\|/, '').replace(/\|$/, '');
|
|
74
|
+
return inner.split('|').map(c => c.trim());
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Collect the table-row lines belonging to the section under `heading`,
|
|
78
|
+
* stopping at the next `## ` heading (or, for Requirements, the rollup
|
|
79
|
+
* marker — defensive; the canonical template puts markers under their own
|
|
80
|
+
* heading). Returns null when the heading itself is absent.
|
|
81
|
+
*/
|
|
82
|
+
function sectionRows(lines, heading) {
|
|
83
|
+
const start = lines.findIndex(l => l.trim() === heading);
|
|
84
|
+
if (start === -1)
|
|
85
|
+
return null;
|
|
86
|
+
const rows = [];
|
|
87
|
+
for (let i = start + 1; i < lines.length; i += 1) {
|
|
88
|
+
const line = lines[i];
|
|
89
|
+
const trimmed = line.trim();
|
|
90
|
+
if (trimmed.startsWith('## ') || trimmed === ROLLUP_OPEN)
|
|
91
|
+
break;
|
|
92
|
+
if (!trimmed.includes('|'))
|
|
93
|
+
continue;
|
|
94
|
+
// Skip markdown table separator rows and header rows immediately preceding them.
|
|
95
|
+
if (SEPARATOR_ROW_RE.test(line))
|
|
96
|
+
continue;
|
|
97
|
+
if (i + 1 < lines.length && SEPARATOR_ROW_RE.test(lines[i + 1]))
|
|
98
|
+
continue;
|
|
99
|
+
rows.push({ line, num: i + 1 });
|
|
100
|
+
}
|
|
101
|
+
return rows;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Parse a `_requirements.md` artifact into registry entries and requirement
|
|
105
|
+
* rows. Fail-loud (D-08): ALL grammar violations are collected before the
|
|
106
|
+
* ok:false return — never warn-and-skip.
|
|
107
|
+
*/
|
|
108
|
+
function parseRequirementsArtifact(content) {
|
|
109
|
+
const errors = [];
|
|
110
|
+
if (!content || typeof content !== 'string') {
|
|
111
|
+
return { ok: false, errors: [{ row: 0, cell: '', reason: 'missing_section' }] };
|
|
112
|
+
}
|
|
113
|
+
const lines = content.split(/\r?\n/);
|
|
114
|
+
const registryRows = sectionRows(lines, REGISTRY_HEADING);
|
|
115
|
+
const requirementRows = sectionRows(lines, REQUIREMENTS_HEADING);
|
|
116
|
+
if (registryRows === null)
|
|
117
|
+
errors.push({ row: 0, cell: REGISTRY_HEADING, reason: 'missing_section' });
|
|
118
|
+
if (requirementRows === null)
|
|
119
|
+
errors.push({ row: 0, cell: REQUIREMENTS_HEADING, reason: 'missing_section' });
|
|
120
|
+
if (registryRows === null || requirementRows === null)
|
|
121
|
+
return { ok: false, errors };
|
|
122
|
+
const registry = [];
|
|
123
|
+
// Registered prefixes match ^[A-Z][A-Z0-9]*-$ (validated below), which
|
|
124
|
+
// excludes __proto__/constructor-shaped keys; lookups still go through a
|
|
125
|
+
// Set/Map, never a plain {} keyed by parsed content (engine convention).
|
|
126
|
+
const registeredPrefixes = new Set();
|
|
127
|
+
for (const { line, num } of registryRows) {
|
|
128
|
+
const cells = splitCells(line);
|
|
129
|
+
const prefix = cells[0] ?? '';
|
|
130
|
+
if (!REGISTRY_PREFIX_RE.test(prefix)) {
|
|
131
|
+
errors.push({ row: num, cell: prefix, reason: 'bad_registry_row' });
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
// D-44: ordering is load-bearing — a reserved prefix must never reach
|
|
135
|
+
// registeredPrefixes, or its requirement rows parse cleanly and only the
|
|
136
|
+
// registry row errors, which reads confusingly.
|
|
137
|
+
if (RESERVED_PREFIXES.has(prefix)) {
|
|
138
|
+
errors.push({ row: num, cell: prefix, reason: 'reserved_prefix' });
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
registry.push({ prefix, domain: cells[1] ?? '' });
|
|
142
|
+
registeredPrefixes.add(prefix);
|
|
143
|
+
}
|
|
144
|
+
const rows = [];
|
|
145
|
+
const seenIds = new Set();
|
|
146
|
+
for (const { line, num } of requirementRows) {
|
|
147
|
+
const cells = splitCells(line);
|
|
148
|
+
const idCell = cells[0] ?? '';
|
|
149
|
+
const reqCell = cells[1] ?? '';
|
|
150
|
+
const moscowCell = cells[2] ?? '';
|
|
151
|
+
const dispositionCell = cells[3] ?? '';
|
|
152
|
+
let rowErrors = 0;
|
|
153
|
+
// ID chain: grammar → registry membership → uniqueness. First failure
|
|
154
|
+
// wins for this axis; the row is still checked on the other axes.
|
|
155
|
+
let prefix = '';
|
|
156
|
+
let seq = 0;
|
|
157
|
+
const idMatch = ROW_ID_RE.exec(idCell);
|
|
158
|
+
if (!idMatch) {
|
|
159
|
+
errors.push({ row: num, cell: idCell, reason: 'bad_id' });
|
|
160
|
+
rowErrors += 1;
|
|
161
|
+
}
|
|
162
|
+
else if (!registeredPrefixes.has(idMatch[1])) {
|
|
163
|
+
errors.push({ row: num, cell: idCell, reason: 'unknown_prefix' });
|
|
164
|
+
rowErrors += 1;
|
|
165
|
+
}
|
|
166
|
+
else if (seenIds.has(idCell)) {
|
|
167
|
+
errors.push({ row: num, cell: idCell, reason: 'duplicate_id' });
|
|
168
|
+
rowErrors += 1;
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
seenIds.add(idCell);
|
|
172
|
+
prefix = idMatch[1];
|
|
173
|
+
seq = parseInt(idMatch[2], 10);
|
|
174
|
+
}
|
|
175
|
+
// MoSCoW: closed enum, case-normalized on parse (D-03).
|
|
176
|
+
const moscow = moscowCell
|
|
177
|
+
? moscowCell.charAt(0).toUpperCase() + moscowCell.slice(1).toLowerCase()
|
|
178
|
+
: '';
|
|
179
|
+
if (!MOSCOW_SET.has(moscow)) {
|
|
180
|
+
errors.push({ row: num, cell: moscowCell, reason: 'bad_moscow' });
|
|
181
|
+
rowErrors += 1;
|
|
182
|
+
}
|
|
183
|
+
// Disposition: exactly MVP, Later, or Dropped (D-<ss>-<n>). Bare
|
|
184
|
+
// `Dropped` without a killing decision ID is an error (D-03, D-04).
|
|
185
|
+
let disposition = '';
|
|
186
|
+
let droppedDecisionId = null;
|
|
187
|
+
if (dispositionCell === DISPOSITIONS.MVP || dispositionCell === DISPOSITIONS.LATER) {
|
|
188
|
+
disposition = dispositionCell;
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
const dm = DROPPED_RE.exec(dispositionCell);
|
|
192
|
+
if (dm) {
|
|
193
|
+
disposition = DISPOSITIONS.DROPPED;
|
|
194
|
+
droppedDecisionId = dm[1];
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
errors.push({ row: num, cell: dispositionCell, reason: 'bad_disposition' });
|
|
198
|
+
rowErrors += 1;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
// Strike consistency: ~~...~~ if-and-only-if Dropped (checked only when
|
|
202
|
+
// the disposition itself parsed — a bad disposition already errored).
|
|
203
|
+
if (disposition) {
|
|
204
|
+
const struck = STRIKE_RE.test(reqCell);
|
|
205
|
+
const dropped = disposition === DISPOSITIONS.DROPPED;
|
|
206
|
+
if (struck !== dropped) {
|
|
207
|
+
errors.push({ row: num, cell: reqCell, reason: 'strike_mismatch' });
|
|
208
|
+
rowErrors += 1;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
// Provenance (REQM-05, D-26): origin plus an optional single relation
|
|
212
|
+
// suffix, split on the exact ` · ` separator. An empty or unparseable
|
|
213
|
+
// cell is a structured error — every requirement answers "where did you
|
|
214
|
+
// come from"; no warn-and-skip (D-08). Header text is not parsed
|
|
215
|
+
// (positional contract), so pre-rename artifacts whose fifth cells hold
|
|
216
|
+
// valid decision/session refs keep parsing.
|
|
217
|
+
const provenanceCell = (cells[4] ?? '').trim();
|
|
218
|
+
let provenance = null;
|
|
219
|
+
const parts = provenanceCell.split(PROVENANCE_SEP);
|
|
220
|
+
if (parts.length > 2 || parts[0] === '') {
|
|
221
|
+
errors.push({ row: num, cell: provenanceCell, reason: 'bad_provenance' });
|
|
222
|
+
rowErrors += 1;
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
const origin = parts[0];
|
|
226
|
+
let kind = null;
|
|
227
|
+
if (ACCESSION_CITE_RE.test(origin))
|
|
228
|
+
kind = 'accession';
|
|
229
|
+
else if (SESSION_REF_RE.test(origin))
|
|
230
|
+
kind = 'session';
|
|
231
|
+
else if (PROVENANCE_DECISION_RE.test(origin))
|
|
232
|
+
kind = 'decision';
|
|
233
|
+
let relation = null;
|
|
234
|
+
let relationOk = true;
|
|
235
|
+
if (parts.length === 2) {
|
|
236
|
+
const rm = RELATION_RE.exec(parts[1]);
|
|
237
|
+
if (rm) {
|
|
238
|
+
relation = { verb: rm[1] === 'refines' ? 'refines' : 'absorbed into', target: rm[2] };
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
relationOk = false;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (kind === null || !relationOk) {
|
|
245
|
+
errors.push({ row: num, cell: provenanceCell, reason: 'bad_provenance' });
|
|
246
|
+
rowErrors += 1;
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
provenance = { origin, kind, relation };
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
if (rowErrors === 0 && provenance !== null) {
|
|
253
|
+
rows.push({
|
|
254
|
+
id: idCell,
|
|
255
|
+
prefix,
|
|
256
|
+
seq,
|
|
257
|
+
requirement: reqCell,
|
|
258
|
+
moscow,
|
|
259
|
+
disposition,
|
|
260
|
+
droppedDecisionId,
|
|
261
|
+
provenance,
|
|
262
|
+
line: num,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
// Relation-target existence post-pass (D-27): a refines/absorbed-into
|
|
267
|
+
// target must be an ID present in the same table. Any disposition counts —
|
|
268
|
+
// existence is the only check.
|
|
269
|
+
for (const row of rows) {
|
|
270
|
+
const relation = row.provenance.relation;
|
|
271
|
+
if (relation && !seenIds.has(relation.target)) {
|
|
272
|
+
errors.push({
|
|
273
|
+
row: row.line,
|
|
274
|
+
cell: `${row.provenance.origin}${PROVENANCE_SEP}${relation.verb} ${relation.target}`,
|
|
275
|
+
reason: 'unknown_relation_target',
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
if (errors.length > 0)
|
|
280
|
+
return { ok: false, errors };
|
|
281
|
+
return { ok: true, registry, rows };
|
|
282
|
+
}
|
|
283
|
+
// ─── Rollup computation ───────────────────────────────────────────────────────
|
|
284
|
+
/**
|
|
285
|
+
* Bucket parsed rows per registry prefix × disposition. Buckets are keyed in
|
|
286
|
+
* registry order; IDs sort ascending by numeric sequence. next_id per prefix
|
|
287
|
+
* is max sequence + 1 (1 for a prefix with zero rows) — D-12: the table IS
|
|
288
|
+
* the counter.
|
|
289
|
+
*/
|
|
290
|
+
function computeRollup(rows, registry) {
|
|
291
|
+
const bucketMap = new Map();
|
|
292
|
+
for (const entry of registry) {
|
|
293
|
+
bucketMap.set(entry.prefix, { MVP: [], Later: [], Dropped: [] });
|
|
294
|
+
}
|
|
295
|
+
for (const row of rows) {
|
|
296
|
+
let bucket = bucketMap.get(row.prefix);
|
|
297
|
+
if (!bucket) {
|
|
298
|
+
// Defensive: parse guarantees registry membership, but keep unknown
|
|
299
|
+
// prefixes countable rather than silently dropping rows.
|
|
300
|
+
bucket = { MVP: [], Later: [], Dropped: [] };
|
|
301
|
+
bucketMap.set(row.prefix, bucket);
|
|
302
|
+
}
|
|
303
|
+
if (row.disposition === DISPOSITIONS.MVP)
|
|
304
|
+
bucket.MVP.push(row);
|
|
305
|
+
else if (row.disposition === DISPOSITIONS.LATER)
|
|
306
|
+
bucket.Later.push(row);
|
|
307
|
+
else
|
|
308
|
+
bucket.Dropped.push(row);
|
|
309
|
+
}
|
|
310
|
+
const counts = {};
|
|
311
|
+
const nextId = {};
|
|
312
|
+
const totals = { MVP: 0, Later: 0, Dropped: 0, total: 0 };
|
|
313
|
+
const bySeq = (a, b) => a.seq - b.seq;
|
|
314
|
+
for (const [prefix, bucket] of bucketMap) {
|
|
315
|
+
counts[prefix] = {
|
|
316
|
+
MVP: bucket.MVP.slice().sort(bySeq).map(r => r.id),
|
|
317
|
+
Later: bucket.Later.slice().sort(bySeq).map(r => r.id),
|
|
318
|
+
Dropped: bucket.Dropped.slice().sort(bySeq).map(r => r.id),
|
|
319
|
+
};
|
|
320
|
+
totals.MVP += bucket.MVP.length;
|
|
321
|
+
totals.Later += bucket.Later.length;
|
|
322
|
+
totals.Dropped += bucket.Dropped.length;
|
|
323
|
+
const maxSeq = [...bucket.MVP, ...bucket.Later, ...bucket.Dropped]
|
|
324
|
+
.reduce((max, r) => (r.seq > max ? r.seq : max), 0);
|
|
325
|
+
nextId[prefix] = maxSeq + 1;
|
|
326
|
+
}
|
|
327
|
+
totals.total = totals.MVP + totals.Later + totals.Dropped;
|
|
328
|
+
return { counts, totals, next_id: nextId };
|
|
329
|
+
}
|
|
330
|
+
// ─── Rendering ────────────────────────────────────────────────────────────────
|
|
331
|
+
function renderCell(ids) {
|
|
332
|
+
return ids.length > 0 ? `${ids.length} (${ids.join(', ')})` : '0';
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Render the machine-owned totals table (D-06). Deterministic: registry
|
|
336
|
+
* order, then a bold Total row — the workflow pastes this verbatim between
|
|
337
|
+
* the rollup:auto markers.
|
|
338
|
+
*/
|
|
339
|
+
function renderRollupTable(rollup) {
|
|
340
|
+
const lines = [
|
|
341
|
+
'| Prefix | MVP | Later | Dropped | Total |',
|
|
342
|
+
'|--------|-----|-------|---------|-------|',
|
|
343
|
+
];
|
|
344
|
+
for (const prefix of Object.keys(rollup.counts)) {
|
|
345
|
+
const bucket = rollup.counts[prefix];
|
|
346
|
+
const rowTotal = bucket.MVP.length + bucket.Later.length + bucket.Dropped.length;
|
|
347
|
+
lines.push(`| ${prefix} | ${renderCell(bucket.MVP)} | ${renderCell(bucket.Later)} | ${renderCell(bucket.Dropped)} | ${rowTotal} |`);
|
|
348
|
+
}
|
|
349
|
+
const t = rollup.totals;
|
|
350
|
+
lines.push(`| **Total** | ${t.MVP} | ${t.Later} | ${t.Dropped} | ${t.total} |`);
|
|
351
|
+
return lines.join('\n');
|
|
352
|
+
}
|
|
353
|
+
// ─── Check (D-07) ─────────────────────────────────────────────────────────────
|
|
354
|
+
/** Whitespace-normalize for block comparison: trim lines, collapse runs, drop blanks. */
|
|
355
|
+
function normalizeBlock(text) {
|
|
356
|
+
return text
|
|
357
|
+
.split(/\r?\n/)
|
|
358
|
+
.map(l => l.trim().replace(/\s+/g, ' '))
|
|
359
|
+
.filter(l => l.length > 0)
|
|
360
|
+
.join('\n');
|
|
361
|
+
}
|
|
362
|
+
function countOccurrences(haystack, needle) {
|
|
363
|
+
return haystack.split(needle).length - 1;
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Verify the written rollup:auto block against totals recomputed from the
|
|
367
|
+
* rows (D-07). Missing/duplicated/out-of-order markers and parse failures
|
|
368
|
+
* are structured failures too — a block that cannot be located or a table
|
|
369
|
+
* that cannot be counted is drift by definition.
|
|
370
|
+
*/
|
|
371
|
+
function checkRollup(content) {
|
|
372
|
+
const parsed = parseRequirementsArtifact(content);
|
|
373
|
+
if (!parsed.ok)
|
|
374
|
+
return { ok: false, reason: 'parse_error', errors: parsed.errors };
|
|
375
|
+
const openCount = countOccurrences(content, ROLLUP_OPEN);
|
|
376
|
+
const closeCount = countOccurrences(content, ROLLUP_CLOSE);
|
|
377
|
+
if (openCount !== 1 || closeCount !== 1) {
|
|
378
|
+
return {
|
|
379
|
+
ok: false,
|
|
380
|
+
reason: 'rollup_markers',
|
|
381
|
+
detail: `expected exactly one ${ROLLUP_OPEN} and one ${ROLLUP_CLOSE} (found ${openCount}/${closeCount})`,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
const openIdx = content.indexOf(ROLLUP_OPEN);
|
|
385
|
+
const closeIdx = content.indexOf(ROLLUP_CLOSE);
|
|
386
|
+
if (closeIdx < openIdx) {
|
|
387
|
+
return { ok: false, reason: 'rollup_markers', detail: 'close marker precedes open marker' };
|
|
388
|
+
}
|
|
389
|
+
const rollup = computeRollup(parsed.rows, parsed.registry);
|
|
390
|
+
const expected = normalizeBlock(renderRollupTable(rollup));
|
|
391
|
+
const actual = normalizeBlock(content.slice(openIdx + ROLLUP_OPEN.length, closeIdx));
|
|
392
|
+
if (expected !== actual) {
|
|
393
|
+
return { ok: false, reason: 'rollup_mismatch', expected, actual };
|
|
394
|
+
}
|
|
395
|
+
return { ok: true, totals: rollup.totals };
|
|
396
|
+
}
|
|
397
|
+
// ─── CLI adapter ──────────────────────────────────────────────────────────────
|
|
398
|
+
function formatRollupErrors(errors) {
|
|
399
|
+
return errors.map(e => ` row ${e.row}: ${e.reason}${e.cell ? ` (${e.cell})` : ''}`).join('\n');
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* CLI adapter for `pp-tools discovery rollup <path> [--check]`.
|
|
403
|
+
*
|
|
404
|
+
* Exit semantics (D-06/D-07/D-08): plain mode is the recompute source — it
|
|
405
|
+
* exits 0 with JSON + rendered table even when the written block is stale,
|
|
406
|
+
* but fails loud (non-zero) on any parse error. `--check` exits non-zero on
|
|
407
|
+
* drift, marker problems, or parse errors, via error() which honors
|
|
408
|
+
* --json-errors.
|
|
409
|
+
*/
|
|
410
|
+
function cmdDiscoveryRollup(cwd, args, raw) {
|
|
411
|
+
const check = args.includes('--check');
|
|
412
|
+
for (const a of args.slice(2)) {
|
|
413
|
+
if (a.startsWith('--') && a !== '--check') {
|
|
414
|
+
error(`discovery rollup: unknown flag ${a}`, ERROR_REASON.USAGE);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
const target = args.slice(2).find(a => !a.startsWith('--'));
|
|
418
|
+
if (!target) {
|
|
419
|
+
error('Usage: pp-tools discovery rollup <path> [--check]', ERROR_REASON.USAGE);
|
|
420
|
+
return; // unreachable — error() exits
|
|
421
|
+
}
|
|
422
|
+
// Path-traversal guard: the resolved target must stay under the project
|
|
423
|
+
// root (threat T-01-01; mirrors the workstream-inventory guard convention).
|
|
424
|
+
const root = node_path_1.default.resolve(cwd);
|
|
425
|
+
const resolved = node_path_1.default.resolve(cwd, target);
|
|
426
|
+
if (!resolved.startsWith(root + node_path_1.default.sep)) {
|
|
427
|
+
error('discovery rollup: path escapes project root', ERROR_REASON.USAGE);
|
|
428
|
+
}
|
|
429
|
+
let content = '';
|
|
430
|
+
try {
|
|
431
|
+
content = node_fs_1.default.readFileSync(resolved, 'utf-8');
|
|
432
|
+
}
|
|
433
|
+
catch {
|
|
434
|
+
error(`discovery rollup: cannot read ${target}`, ERROR_REASON.USAGE);
|
|
435
|
+
}
|
|
436
|
+
if (check) {
|
|
437
|
+
const result = checkRollup(content);
|
|
438
|
+
if (!result.ok) {
|
|
439
|
+
if (result.reason === 'parse_error') {
|
|
440
|
+
error(`discovery rollup --check: parse failed:\n${formatRollupErrors(result.errors)}`, ERROR_REASON.DISCOVERY_ROLLUP_PARSE_FAILED);
|
|
441
|
+
return; // unreachable — error() exits; return keeps tsc narrowing sound
|
|
442
|
+
}
|
|
443
|
+
const detail = result.reason === 'rollup_markers'
|
|
444
|
+
? result.detail
|
|
445
|
+
: `expected:\n${result.expected}\nactual:\n${result.actual}`;
|
|
446
|
+
error(`discovery rollup --check: ${result.reason}\n${detail}`, ERROR_REASON.DISCOVERY_ROLLUP_MISMATCH);
|
|
447
|
+
return; // unreachable — error() exits
|
|
448
|
+
}
|
|
449
|
+
output({ ok: true, checked: true, totals: result.totals }, raw);
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
const parsed = parseRequirementsArtifact(content);
|
|
453
|
+
if (!parsed.ok) {
|
|
454
|
+
error(`discovery rollup: parse failed:\n${formatRollupErrors(parsed.errors)}`, ERROR_REASON.DISCOVERY_ROLLUP_PARSE_FAILED);
|
|
455
|
+
return; // unreachable — error() exits
|
|
456
|
+
}
|
|
457
|
+
const rollup = computeRollup(parsed.rows, parsed.registry);
|
|
458
|
+
const table = renderRollupTable(rollup);
|
|
459
|
+
output({
|
|
460
|
+
ok: true,
|
|
461
|
+
registry: parsed.registry,
|
|
462
|
+
counts: rollup.counts,
|
|
463
|
+
totals: rollup.totals,
|
|
464
|
+
next_id: rollup.next_id,
|
|
465
|
+
table,
|
|
466
|
+
}, raw, table);
|
|
467
|
+
}
|
|
468
|
+
module.exports = {
|
|
469
|
+
DISPOSITIONS,
|
|
470
|
+
MOSCOW,
|
|
471
|
+
parseRequirementsArtifact,
|
|
472
|
+
computeRollup,
|
|
473
|
+
renderRollupTable,
|
|
474
|
+
checkRollup,
|
|
475
|
+
cmdDiscoveryRollup,
|
|
476
|
+
};
|