@pareto4/pp-core 1.19.0 → 1.21.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-nyquist-auditor.md +19 -14
- package/agents/pp-phase-researcher.md +10 -0
- package/agents/pp-plan-checker.md +59 -36
- package/agents/pp-planner.md +49 -53
- package/agents/pp-vault-reviewer.md +2 -0
- package/agents/pp-verifier.md +15 -60
- package/commands/pp-autonomous.md +2 -1
- package/commands/pp-discovery.md +3 -2
- package/commands/pp-plan-phase.md +2 -1
- package/commands/pp-plan-review-convergence.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-activation.cjs +77 -0
- package/pp-core/bin/lib/capability-registry.cjs +15 -15
- package/pp-core/bin/lib/capability-state.cjs +8 -18
- package/pp-core/bin/lib/ceremony.cjs +1342 -0
- package/pp-core/bin/lib/check-command-router.cjs +62 -1
- package/pp-core/bin/lib/commands.cjs +29 -3
- package/pp-core/bin/lib/config.cjs +5 -0
- 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 +8 -7
- package/pp-core/bin/lib/discovery-rules.cjs +216 -0
- package/pp-core/bin/lib/discovery-screens.cjs +1 -1
- 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 +1 -1
- package/pp-core/bin/lib/discovery-table.cjs +55 -0
- package/pp-core/bin/lib/init.cjs +50 -1
- package/pp-core/bin/lib/io.cjs +24 -0
- package/pp-core/bin/lib/loop-resolver.cjs +26 -12
- package/pp-core/bin/lib/phase.cjs +39 -3
- package/pp-core/bin/lib/plan-sizing.cjs +346 -0
- package/pp-core/bin/lib/test-substance.cjs +3190 -0
- package/pp-core/bin/lib/timeline.cjs +605 -0
- package/pp-core/bin/lib/vault-standards-review.cjs +50 -2
- package/pp-core/bin/pp-tools.cjs +214 -5
- package/pp-core/bin/shared/ceremony-gates.json +17 -0
- package/pp-core/bin/shared/config-schema.manifest.json +1 -0
- package/pp-core/references/ceremony.md +81 -0
- 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 +50 -14
- 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/few-shot-examples/plan-checker.md +8 -8
- package/pp-core/references/phase-timeline.md +107 -0
- package/pp-core/references/planner-antipatterns.md +32 -0
- package/pp-core/references/planner-discovery-extract.md +35 -0
- package/pp-core/references/planner-guidance.md +17 -14
- package/pp-core/references/planner-revision.md +1 -1
- package/pp-core/references/planning-config.md +3 -2
- package/pp-core/references/test-substance.md +324 -0
- package/pp-core/references/verifier-discovery-conformance.md +36 -0
- package/pp-core/references/verifier-stub-patterns.md +51 -0
- package/pp-core/templates/VALIDATION.md +1 -0
- package/pp-core/templates/phase-prompt.md +19 -7
- 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/roadmap.md +1 -1
- package/pp-core/templates/verification-report.md +17 -1
- package/pp-core/workflows/audit-milestone.md +4 -1
- package/pp-core/workflows/autonomous.md +11 -8
- package/pp-core/workflows/code-review.md +15 -6
- 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 +10 -0
- package/pp-core/workflows/discovery-sprint/sessions/01-kick-off.md +5 -2
- package/pp-core/workflows/discovery-sprint/sessions/02-as-is.md +27 -5
- package/pp-core/workflows/discovery-sprint/sessions/03-to-be.md +56 -31
- package/pp-core/workflows/discovery-sprint/sessions/04-ux-design.md +1 -1
- package/pp-core/workflows/discovery-sprint/sessions/05-architecture.md +42 -19
- package/pp-core/workflows/discovery-sprint/sessions/06-security.md +27 -12
- package/pp-core/workflows/discovery-sprint/sessions/07-identity.md +41 -23
- package/pp-core/workflows/discovery-sprint/sessions/08-integration.md +50 -23
- package/pp-core/workflows/discovery-sprint/sessions/09-qa.md +26 -11
- package/pp-core/workflows/discovery-sprint/sessions/10-infrastructure.md +28 -9
- package/pp-core/workflows/discovery-sprint/sessions/11-roadmap.md +21 -0
- package/pp-core/workflows/discovery-sprint/sessions/13-wrap-up.md +24 -9
- package/pp-core/workflows/discovery-sprint.md +12 -15
- package/pp-core/workflows/discovery-to-build/steps/architecture-deliverables.md +80 -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.md +10 -7
- package/pp-core/workflows/execute-phase/steps/check-query-gate.md +7 -7
- package/pp-core/workflows/execute-phase/steps/codebase-drift-gate.md +3 -2
- package/pp-core/workflows/execute-phase.md +14 -3
- package/pp-core/workflows/new-project.md +3 -3
- package/pp-core/workflows/plan-phase/steps/chunked-planning.md +7 -3
- package/pp-core/workflows/plan-phase/steps/discovery-extract.md +41 -0
- package/pp-core/workflows/plan-phase/steps/prd-express.md +104 -0
- package/pp-core/workflows/plan-phase.md +62 -124
- package/pp-core/workflows/plan-review-convergence.md +6 -3
- package/pp-core/workflows/ship.md +2 -2
- package/pp-core/workflows/stats.md +19 -4
- package/pp-core/workflows/validate-phase.md +19 -4
- package/pp-core/workflows/vault-review.md +5 -0
- package/pp-core/workflows/verify-phase.md +1 -1
- package/pp-core/workflows/verify-work.md +1 -1
- package/scripts/gen-capability-registry.cjs +43 -45
- package/scripts/run-tests.cjs +54 -30
- package/pp-core/workflows/discovery-sprint/templates/context-template.md +0 -272
|
@@ -23,6 +23,8 @@ const phaseScopeMod = require("./phase-scope.cjs");
|
|
|
23
23
|
const { resolvePhaseForRead } = phaseScopeMod;
|
|
24
24
|
const decisions_cjs_1 = require("./decisions.cjs");
|
|
25
25
|
const ui_safety_gate_cjs_1 = require("./ui-safety-gate.cjs");
|
|
26
|
+
const plan_sizing_cjs_1 = require("./plan-sizing.cjs");
|
|
27
|
+
const test_substance_cjs_1 = require("./test-substance.cjs");
|
|
26
28
|
const semver_compare_cjs_1 = require("./semver-compare.cjs");
|
|
27
29
|
// Quick 261003-lqi (FIX-C): the ship-time drift check resolves the platform tag
|
|
28
30
|
// through the init-time owner, so it and init cannot disagree about the tag.
|
|
@@ -663,6 +665,57 @@ function cmdUiPlanGate(projectDir, args, raw) {
|
|
|
663
665
|
}
|
|
664
666
|
output(computeUiPlanGate(projectDir, phase), raw, undefined);
|
|
665
667
|
}
|
|
668
|
+
// ─── plan-sizing ──────────────────────────────────────────────────────────────
|
|
669
|
+
/**
|
|
670
|
+
* plan-sizing: reads a phase's PLAN.md files and `context_window` and reports plans
|
|
671
|
+
* over the 6-task ceiling, plans over the per-plan budget, and serial splits that
|
|
672
|
+
* have neither a parallelism nor a budget reason (Phase 36, D-14).
|
|
673
|
+
*
|
|
674
|
+
* Invocable as: pp_run check plan-sizing <phase>
|
|
675
|
+
* Exits 0 on every analysed phase; `block` carries the verdict in the payload.
|
|
676
|
+
*/
|
|
677
|
+
function cmdPlanSizing(projectDir, args, raw) {
|
|
678
|
+
// args[0] = 'check', args[1] = 'plan-sizing', args[2] = phase
|
|
679
|
+
const phase = args[2] || '';
|
|
680
|
+
if (!phase) {
|
|
681
|
+
error('plan-sizing requires a phase argument: check plan-sizing <phase>', ERROR_REASON.SDK_MISSING_ARG);
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
output((0, plan_sizing_cjs_1.computePlanSizing)(projectDir, phase), raw, undefined);
|
|
685
|
+
}
|
|
686
|
+
// ─── test-substance ───────────────────────────────────────────────────────────
|
|
687
|
+
/**
|
|
688
|
+
* test-substance: reads a phase's PLAN.md files and reports, per task, a missing or
|
|
689
|
+
* generic <behavior>, a verify command that can pass without asserting, a MISSING
|
|
690
|
+
* test file no task creates and a scaffold that describes a vacuous test
|
|
691
|
+
* (Phase 38, D-06).
|
|
692
|
+
*
|
|
693
|
+
* Invocable as: pp_run check test-substance <phase>
|
|
694
|
+
* Exits 0 on every analysed phase; `block` carries the verdict in the payload.
|
|
695
|
+
*
|
|
696
|
+
* The scan mode, `check test-substance --scan <file...>`, counts assertions per test
|
|
697
|
+
* case in the given test files (Phase 38, D-14): the validation-time half, read by
|
|
698
|
+
* validate-phase. It exits 0 on every scanned list; a file the scanner cannot read
|
|
699
|
+
* reports `unknown` with a reason.
|
|
700
|
+
*/
|
|
701
|
+
function cmdTestSubstance(projectDir, args, raw) {
|
|
702
|
+
// args[0] = 'check', args[1] = 'test-substance', args[2..] = phase (flags skipped) or --scan <file...>
|
|
703
|
+
if (args[2] === '--scan') {
|
|
704
|
+
const files = args.slice(3).filter((a) => !a.startsWith('--'));
|
|
705
|
+
if (files.length === 0) {
|
|
706
|
+
error('test-substance --scan requires at least one test file: check test-substance --scan <file...>', ERROR_REASON.SDK_MISSING_ARG);
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
output((0, test_substance_cjs_1.scanTestFiles)(projectDir, files), raw, undefined);
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
const phase = args.slice(2).find((a) => !a.startsWith('--')) || '';
|
|
713
|
+
if (!phase) {
|
|
714
|
+
error('test-substance requires a phase argument or test files: check test-substance <phase> | check test-substance --scan <file...>', ERROR_REASON.SDK_MISSING_ARG);
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
output((0, test_substance_cjs_1.computeTestSubstance)(projectDir, phase), raw, undefined);
|
|
718
|
+
}
|
|
666
719
|
// ─── ui-safety-gate ───────────────────────────────────────────────────────────
|
|
667
720
|
/**
|
|
668
721
|
* ui-safety-gate: post-wave check that verifies UI-changed files conform to
|
|
@@ -3105,6 +3158,14 @@ function routeCheckCommand({ args, cwd, raw }) {
|
|
|
3105
3158
|
cmdUiPlanGate(cwd, args, raw);
|
|
3106
3159
|
return;
|
|
3107
3160
|
}
|
|
3161
|
+
if (subcommand === 'plan-sizing') {
|
|
3162
|
+
cmdPlanSizing(cwd, args, raw);
|
|
3163
|
+
return;
|
|
3164
|
+
}
|
|
3165
|
+
if (subcommand === 'test-substance') {
|
|
3166
|
+
cmdTestSubstance(cwd, args, raw);
|
|
3167
|
+
return;
|
|
3168
|
+
}
|
|
3108
3169
|
if (subcommand === 'gap-analysis-plan-post') {
|
|
3109
3170
|
cmdGapAnalysisPlanPost(cwd, args, raw);
|
|
3110
3171
|
return;
|
|
@@ -3175,7 +3236,7 @@ function routeCheckCommand({ args, cwd, raw }) {
|
|
|
3175
3236
|
(0, prohibition_enforcement_cjs_1.routeProhibitionEnforcement)(args, raw);
|
|
3176
3237
|
return;
|
|
3177
3238
|
}
|
|
3178
|
-
error('Unknown check subcommand. Available: auto-mode, decision-coverage-plan, decision-coverage-verify, gap-analysis-plan-post, prohibition-enforcement, ship-platform-drift, tdd-review-checkpoint, ui-plan-gate, ui-safety-gate, vault-review, vault-status, vault-trace, verify-schema-drift, verify-codebase-drift', ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
|
3239
|
+
error('Unknown check subcommand. Available: auto-mode, decision-coverage-plan, decision-coverage-verify, gap-analysis-plan-post, plan-sizing, prohibition-enforcement, ship-platform-drift, tdd-review-checkpoint, test-substance, ui-plan-gate, ui-safety-gate, vault-review, vault-status, vault-trace, verify-schema-drift, verify-codebase-drift', ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
|
3179
3240
|
}
|
|
3180
3241
|
module.exports = {
|
|
3181
3242
|
routeCheckCommand,
|
|
@@ -51,6 +51,10 @@ const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
|
|
|
51
51
|
// Node built-ins and it never requires this module, so the edge adds no cycle.
|
|
52
52
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
53
53
|
const vaultSeal = require("./vault-seal.cjs");
|
|
54
|
+
// 35-02: per-phase TIMELINE.jsonl fold for cmdStats (timeline key + wall_clock column).
|
|
55
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
56
|
+
const timelineMod = require("./timeline.cjs");
|
|
57
|
+
const { foldRecords, buildPhaseTimeline } = timelineMod;
|
|
54
58
|
// ─── Phase Status ─────────────────────────────────────────────────────────────
|
|
55
59
|
/**
|
|
56
60
|
* Determine phase status by checking plan/summary counts AND verification state.
|
|
@@ -1141,6 +1145,7 @@ function cmdStats(cwd, format, raw) {
|
|
|
1141
1145
|
const isDirInMilestone = getMilestonePhaseFilter(cwd);
|
|
1142
1146
|
// Phase & plan stats (reuse progress pattern)
|
|
1143
1147
|
const phasesByNumber = new Map();
|
|
1148
|
+
const timeline = {};
|
|
1144
1149
|
let totalPlans = 0;
|
|
1145
1150
|
let totalSummaries = 0;
|
|
1146
1151
|
try {
|
|
@@ -1160,6 +1165,7 @@ function cmdStats(cwd, format, raw) {
|
|
|
1160
1165
|
plans: 0,
|
|
1161
1166
|
summaries: 0,
|
|
1162
1167
|
status: 'Not Started',
|
|
1168
|
+
wall_clock: null,
|
|
1163
1169
|
});
|
|
1164
1170
|
}
|
|
1165
1171
|
}
|
|
@@ -1186,12 +1192,31 @@ function cmdStats(cwd, format, raw) {
|
|
|
1186
1192
|
const status = determinePhaseStatus(plans, summaries, node_path_1.default.join(phasesDir, dir), 'Not Started');
|
|
1187
1193
|
const normalizedNum = normalizePhaseName(phaseNum);
|
|
1188
1194
|
const existing = phasesByNumber.get(normalizedNum);
|
|
1195
|
+
// D-12/D-15: fold this phase's TIMELINE file(s). Its own try/catch keeps one
|
|
1196
|
+
// unreadable file from escaping into the outer catch and dropping later phases.
|
|
1197
|
+
let wallClock = existing?.wall_clock ?? null;
|
|
1198
|
+
try {
|
|
1199
|
+
const timelineFiles = phaseFiles.filter(f => f.endsWith('-TIMELINE.jsonl'));
|
|
1200
|
+
if (timelineFiles.length > 0) {
|
|
1201
|
+
const text = timelineFiles
|
|
1202
|
+
.map(f => node_fs_1.default.readFileSync(node_path_1.default.join(phasesDir, dir, f), 'utf-8'))
|
|
1203
|
+
.join('\n');
|
|
1204
|
+
const runs = foldRecords(text);
|
|
1205
|
+
if (runs.length > 0) {
|
|
1206
|
+
const built = buildPhaseTimeline(runs);
|
|
1207
|
+
timeline[normalizedNum] = built;
|
|
1208
|
+
wallClock = built.wall_clock;
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
catch { /* a bad timeline file never drops phases */ }
|
|
1189
1213
|
phasesByNumber.set(normalizedNum, {
|
|
1190
1214
|
number: normalizedNum,
|
|
1191
1215
|
name: existing?.name || phaseName,
|
|
1192
1216
|
plans: (existing?.plans || 0) + plans,
|
|
1193
1217
|
summaries: (existing?.summaries || 0) + summaries,
|
|
1194
1218
|
status,
|
|
1219
|
+
wall_clock: wallClock,
|
|
1195
1220
|
});
|
|
1196
1221
|
}
|
|
1197
1222
|
}
|
|
@@ -1240,6 +1265,7 @@ function cmdStats(cwd, format, raw) {
|
|
|
1240
1265
|
milestone_version: milestone.version,
|
|
1241
1266
|
milestone_name: milestone.name,
|
|
1242
1267
|
phases,
|
|
1268
|
+
timeline,
|
|
1243
1269
|
phases_completed: completedPhases,
|
|
1244
1270
|
phases_total: phases.length,
|
|
1245
1271
|
total_plans: totalPlans,
|
|
@@ -1266,10 +1292,10 @@ function cmdStats(cwd, format, raw) {
|
|
|
1266
1292
|
out += `**Requirements:** ${requirementsComplete}/${requirementsTotal} complete\n`;
|
|
1267
1293
|
}
|
|
1268
1294
|
out += '\n';
|
|
1269
|
-
out += `| Phase | Name | Plans | Completed | Status |\n`;
|
|
1270
|
-
out +=
|
|
1295
|
+
out += `| Phase | Name | Plans | Completed | Status | Wall-clock |\n`;
|
|
1296
|
+
out += `|-------|------|-------|-----------|--------|------------|\n`;
|
|
1271
1297
|
for (const p of phases) {
|
|
1272
|
-
out += `| ${p.number} | ${p.name} | ${p.plans} | ${p.summaries} | ${p.status} |\n`;
|
|
1298
|
+
out += `| ${p.number} | ${p.name} | ${p.plans} | ${p.summaries} | ${p.status} | ${p.wall_clock ?? '—'} |\n`;
|
|
1273
1299
|
}
|
|
1274
1300
|
if (gitCommits > 0) {
|
|
1275
1301
|
out += `\n**Git:** ${gitCommits} commits`;
|
|
@@ -589,6 +589,11 @@ function cmdConfigSet(cwd, keyPath, value, raw) {
|
|
|
589
589
|
if (kp === 'workflow.drift_action' && !isStringMember(VALID_DRIFT_ACTIONS, parsedValue)) {
|
|
590
590
|
error(`Invalid workflow.drift_action '${val}'. Valid values: ${VALID_DRIFT_ACTIONS.join(', ')}`);
|
|
591
591
|
}
|
|
592
|
+
// Phase classification (D-09, CLAS-03): `full` reproduces the pre-1.21 gate stack.
|
|
593
|
+
const VALID_CEREMONY_MODES = ['proportional', 'full'];
|
|
594
|
+
if (kp === 'workflow.ceremony' && !isStringMember(VALID_CEREMONY_MODES, parsedValue)) {
|
|
595
|
+
error(`Invalid workflow.ceremony '${val}'. Valid values: ${VALID_CEREMONY_MODES.join(', ')}`);
|
|
596
|
+
}
|
|
592
597
|
if (kp === 'workflow.drift_threshold') {
|
|
593
598
|
if (typeof parsedValue !== 'number' || !Number.isInteger(parsedValue) || parsedValue < 1) {
|
|
594
599
|
error(`Invalid workflow.drift_threshold '${val}'. Must be a positive integer.`);
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Discovery access-model check (Discovery Sprint improvement plan, Batch 3).
|
|
4
|
+
*
|
|
5
|
+
* `.planning/discovery/_roles.md` is the register of roles and what each may
|
|
6
|
+
* do — Session 7 writes it in place of the old preset RBAC matrix. Three
|
|
7
|
+
* tables: `## Roles` (the role, what it is, its data scope, how it is
|
|
8
|
+
* recovered when its last holder leaves — the vault's role-recovery rule —
|
|
9
|
+
* and its source), `## Permissions` (role × operation × entity, with the
|
|
10
|
+
* condition that narrows it and the requirements it realises; one row per
|
|
11
|
+
* role-and-entity, operations as a list — never CRUD columns) and
|
|
12
|
+
* `## Screens` (which role may open which `SCR-` screen, the join the
|
|
13
|
+
* forbidden-path tests are written from). The build's planner reads the
|
|
14
|
+
* permissions by requirement ID. This module checks the structure:
|
|
15
|
+
*
|
|
16
|
+
* - the three sections exist with the template's columns;
|
|
17
|
+
* - every role is a Session 2 persona, an actor a step matrix names, or
|
|
18
|
+
* `System`, when those sources are on file (a role nobody plays is a
|
|
19
|
+
* role the build cannot test), and is listed once;
|
|
20
|
+
* - every permission and screen row names a role of `## Roles`;
|
|
21
|
+
* - every entity resolves against `_entities.md`, every screen against
|
|
22
|
+
* `_screens.md`, every requirement against `_requirements.md`, when
|
|
23
|
+
* those artifacts are on file and parseable;
|
|
24
|
+
* - operations are a comma list of lowercase verbs.
|
|
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
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
42
|
+
const processes = require("./discovery-processes.cjs");
|
|
43
|
+
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
44
|
+
const DISCOVERY_DIR = '.planning/discovery';
|
|
45
|
+
const ROLES_FILE = `${DISCOVERY_DIR}/_roles.md`;
|
|
46
|
+
const REQUIREMENTS_FILE = `${DISCOVERY_DIR}/_requirements.md`;
|
|
47
|
+
const ENTITIES_FILE = `${DISCOVERY_DIR}/_entities.md`;
|
|
48
|
+
const PROCESSES_FILE = `${DISCOVERY_DIR}/_processes.md`;
|
|
49
|
+
const SCREENS_FILE = `${DISCOVERY_DIR}/_screens.md`;
|
|
50
|
+
const SESSION_02_FILE = `${DISCOVERY_DIR}/session-02.md`;
|
|
51
|
+
const ROLES_SECTION = 'Roles';
|
|
52
|
+
const PERMISSIONS_SECTION = 'Permissions';
|
|
53
|
+
const SCREENS_SECTION = 'Screens';
|
|
54
|
+
const ROLES_HEADER = Object.freeze(['Role', 'Description', 'Scope', 'Recovery path', 'Source']);
|
|
55
|
+
const PERMISSIONS_HEADER = Object.freeze(['Role', 'Entity', 'Operation', 'Condition', 'Requirements']);
|
|
56
|
+
const SCREENS_HEADER = Object.freeze(['Role', 'Screen']);
|
|
57
|
+
const SYSTEM_ACTOR = 'System';
|
|
58
|
+
const ENTITY_NAME_RE = /^[A-Z][A-Za-z0-9]*$/;
|
|
59
|
+
const OPERATION_RE = /^[a-z][a-z-]*$/;
|
|
60
|
+
const SCREEN_ID_RE = /^SCR-\d{2}$/;
|
|
61
|
+
const REQUIREMENT_ID_RE = /^[A-Z][A-Z0-9]*-\d+$/;
|
|
62
|
+
const STEPS_ACTOR_COLUMN = 'Actor';
|
|
63
|
+
const USAGE = 'Usage: pp-tools discovery roles-check';
|
|
64
|
+
function readIfPresent(abs) {
|
|
65
|
+
if (!node_fs_1.default.existsSync(abs))
|
|
66
|
+
return { kind: 'absent' };
|
|
67
|
+
try {
|
|
68
|
+
return { kind: 'ok', content: node_fs_1.default.readFileSync(abs, 'utf-8') };
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return { kind: 'unreadable' };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function sameHeader(actual, expected) {
|
|
75
|
+
return actual.length === expected.length && actual.every((c, i) => c.trim().toLowerCase() === expected[i].toLowerCase());
|
|
76
|
+
}
|
|
77
|
+
function splitList(cell) {
|
|
78
|
+
return cell.split(',').map((t) => t.trim()).filter(Boolean);
|
|
79
|
+
}
|
|
80
|
+
/** First-column values of the first table under a `## <heading>` of another artifact. */
|
|
81
|
+
function firstColumnOf(content, heading) {
|
|
82
|
+
const section = table.splitSections(content.split(/\r?\n/), 2).find((s) => s.heading === heading);
|
|
83
|
+
if (!section)
|
|
84
|
+
return new Set();
|
|
85
|
+
const tables = table.parseTables(section.body);
|
|
86
|
+
if (tables.length === 0)
|
|
87
|
+
return new Set();
|
|
88
|
+
return new Set(tables[0].rows.map((r) => (r.cells[0] ?? '').trim()).filter(Boolean));
|
|
89
|
+
}
|
|
90
|
+
/** Every actor a step matrix of `_processes.md` § Steps names, by the `Actor` column. */
|
|
91
|
+
function stepActors(content) {
|
|
92
|
+
const out = new Set();
|
|
93
|
+
const steps = table.splitSections(content.split(/\r?\n/), 2).find((s) => s.heading === 'Steps');
|
|
94
|
+
if (!steps)
|
|
95
|
+
return out;
|
|
96
|
+
for (const t of table.parseTables(steps.body)) {
|
|
97
|
+
const col = t.header.findIndex((h) => h.trim().toLowerCase() === STEPS_ACTOR_COLUMN.toLowerCase());
|
|
98
|
+
if (col === -1)
|
|
99
|
+
continue;
|
|
100
|
+
for (const r of t.rows)
|
|
101
|
+
for (const a of splitList(r.cells[col] ?? ''))
|
|
102
|
+
out.add(a);
|
|
103
|
+
}
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
/** The first table under `## <heading>`, header-checked; faults are pushed and null returned. */
|
|
107
|
+
function sectionTable(content, heading, expected, violations) {
|
|
108
|
+
const section = table.splitSections(content.split(/\r?\n/), 2).find((s) => s.heading === heading);
|
|
109
|
+
if (!section) {
|
|
110
|
+
violations.push({ artifact: ROLES_FILE, reason: 'missing_section', id: heading });
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
const tables = table.parseTables(section.body);
|
|
114
|
+
if (tables.length === 0) {
|
|
115
|
+
violations.push({ artifact: ROLES_FILE, reason: 'missing_section', line: section.line, id: heading });
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
if (!sameHeader(tables[0].header, expected)) {
|
|
119
|
+
violations.push({ artifact: ROLES_FILE, reason: 'bad_table_shape', line: tables[0].line, id: heading });
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
return tables[0];
|
|
123
|
+
}
|
|
124
|
+
// ─── Check ────────────────────────────────────────────────────────────────────
|
|
125
|
+
function checkRoles(content, sources) {
|
|
126
|
+
const violations = [];
|
|
127
|
+
const roles = sectionTable(content, ROLES_SECTION, ROLES_HEADER, violations);
|
|
128
|
+
const permissions = sectionTable(content, PERMISSIONS_SECTION, PERMISSIONS_HEADER, violations);
|
|
129
|
+
const screens = sectionTable(content, SCREENS_SECTION, SCREENS_HEADER, violations);
|
|
130
|
+
const known = new Set();
|
|
131
|
+
if (roles) {
|
|
132
|
+
for (const row of roles.rows) {
|
|
133
|
+
const [role, description, scope, recovery] = row.cells.map((c) => c.trim());
|
|
134
|
+
if ((role ?? '') === '') {
|
|
135
|
+
violations.push({ artifact: ROLES_FILE, reason: 'empty_cell', line: row.line, id: 'Role' });
|
|
136
|
+
}
|
|
137
|
+
else if (known.has(role)) {
|
|
138
|
+
violations.push({ artifact: ROLES_FILE, reason: 'duplicate_role', line: row.line, id: role.slice(0, 40) });
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
known.add(role);
|
|
142
|
+
if (sources.actors !== null && role !== SYSTEM_ACTOR && !sources.actors.has(role))
|
|
143
|
+
violations.push({ artifact: ROLES_FILE, reason: 'unknown_role', line: row.line, id: role.slice(0, 40) });
|
|
144
|
+
}
|
|
145
|
+
if ((description ?? '') === '')
|
|
146
|
+
violations.push({ artifact: ROLES_FILE, reason: 'empty_cell', line: row.line, id: 'Description' });
|
|
147
|
+
if ((scope ?? '') === '')
|
|
148
|
+
violations.push({ artifact: ROLES_FILE, reason: 'empty_cell', line: row.line, id: 'Scope' });
|
|
149
|
+
if ((recovery ?? '') === '')
|
|
150
|
+
violations.push({ artifact: ROLES_FILE, reason: 'empty_cell', line: row.line, id: 'Recovery path' });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const roleRef = (role, line) => {
|
|
154
|
+
if (role === '')
|
|
155
|
+
violations.push({ artifact: ROLES_FILE, reason: 'empty_cell', line, id: 'Role' });
|
|
156
|
+
else if (roles && !known.has(role))
|
|
157
|
+
violations.push({ artifact: ROLES_FILE, reason: 'unknown_role', line, id: role.slice(0, 40) });
|
|
158
|
+
};
|
|
159
|
+
if (permissions) {
|
|
160
|
+
for (const row of permissions.rows) {
|
|
161
|
+
const [role, entity, operation, , requirements] = row.cells.map((c) => c.trim());
|
|
162
|
+
roleRef(role ?? '', row.line);
|
|
163
|
+
if (!ENTITY_NAME_RE.test(entity ?? ''))
|
|
164
|
+
violations.push({ artifact: ROLES_FILE, reason: 'bad_entity', line: row.line });
|
|
165
|
+
else if (sources.entities !== null && !sources.entities.has(entity))
|
|
166
|
+
violations.push({ artifact: ROLES_FILE, reason: 'unknown_entity', line: row.line, id: entity });
|
|
167
|
+
const ops = splitList(operation ?? '');
|
|
168
|
+
if (ops.length === 0 || ops.some((o) => !OPERATION_RE.test(o)))
|
|
169
|
+
violations.push({ artifact: ROLES_FILE, reason: 'bad_operation', line: row.line });
|
|
170
|
+
const reqs = splitList(requirements ?? '');
|
|
171
|
+
if (reqs.length === 0)
|
|
172
|
+
violations.push({ artifact: ROLES_FILE, reason: 'no_requirement', line: row.line });
|
|
173
|
+
for (const r of reqs) {
|
|
174
|
+
if (!REQUIREMENT_ID_RE.test(r))
|
|
175
|
+
violations.push({ artifact: ROLES_FILE, reason: 'unknown_requirement_id', line: row.line, id: r.slice(0, 40) });
|
|
176
|
+
else if (sources.requirementIds instanceof Set && !sources.requirementIds.has(r))
|
|
177
|
+
violations.push({ artifact: ROLES_FILE, reason: 'unknown_requirement_id', line: row.line, id: r });
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (screens) {
|
|
182
|
+
for (const row of screens.rows) {
|
|
183
|
+
const [role, screen] = row.cells.map((c) => c.trim());
|
|
184
|
+
roleRef(role ?? '', row.line);
|
|
185
|
+
if (!SCREEN_ID_RE.test(screen ?? ''))
|
|
186
|
+
violations.push({ artifact: ROLES_FILE, reason: 'bad_screen', line: row.line });
|
|
187
|
+
else if (sources.screens !== null && !sources.screens.has(screen))
|
|
188
|
+
violations.push({ artifact: ROLES_FILE, reason: 'unknown_screen', line: row.line, id: screen });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (sources.requirementIds === 'unparseable')
|
|
192
|
+
violations.push({ artifact: REQUIREMENTS_FILE, reason: 'requirements_unparseable' });
|
|
193
|
+
return {
|
|
194
|
+
ok: violations.length === 0,
|
|
195
|
+
roles: known.size,
|
|
196
|
+
permissions: permissions ? permissions.rows.length : 0,
|
|
197
|
+
screens: screens ? screens.rows.length : 0,
|
|
198
|
+
violations,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
function checkProjectRoles(cwd) {
|
|
202
|
+
const read = readIfPresent(node_path_1.default.join(cwd, ROLES_FILE));
|
|
203
|
+
if (read.kind === 'absent')
|
|
204
|
+
return null;
|
|
205
|
+
if (read.kind === 'unreadable')
|
|
206
|
+
return { ok: false, file: ROLES_FILE, roles: 0, permissions: 0, screens: 0, violations: [{ artifact: ROLES_FILE, reason: 'artifact_unreadable' }] };
|
|
207
|
+
const extra = [];
|
|
208
|
+
const sources = { actors: null, entities: null, screens: null, requirementIds: null };
|
|
209
|
+
const s2 = readIfPresent(node_path_1.default.join(cwd, SESSION_02_FILE));
|
|
210
|
+
if (s2.kind === 'unreadable')
|
|
211
|
+
extra.push({ artifact: SESSION_02_FILE, reason: 'artifact_unreadable' });
|
|
212
|
+
const personas = s2.kind === 'ok' ? processes.personasFrom(s2.content) : null;
|
|
213
|
+
const proc = readIfPresent(node_path_1.default.join(cwd, PROCESSES_FILE));
|
|
214
|
+
if (proc.kind === 'unreadable')
|
|
215
|
+
extra.push({ artifact: PROCESSES_FILE, reason: 'artifact_unreadable' });
|
|
216
|
+
const actors = proc.kind === 'ok' ? stepActors(proc.content) : null;
|
|
217
|
+
if (personas !== null || actors !== null)
|
|
218
|
+
sources.actors = new Set([...(personas ?? []), ...(actors ?? [])]);
|
|
219
|
+
const ent = readIfPresent(node_path_1.default.join(cwd, ENTITIES_FILE));
|
|
220
|
+
if (ent.kind === 'unreadable')
|
|
221
|
+
extra.push({ artifact: ENTITIES_FILE, reason: 'artifact_unreadable' });
|
|
222
|
+
if (ent.kind === 'ok')
|
|
223
|
+
sources.entities = firstColumnOf(ent.content, 'Entities');
|
|
224
|
+
const scr = readIfPresent(node_path_1.default.join(cwd, SCREENS_FILE));
|
|
225
|
+
if (scr.kind === 'unreadable')
|
|
226
|
+
extra.push({ artifact: SCREENS_FILE, reason: 'artifact_unreadable' });
|
|
227
|
+
if (scr.kind === 'ok')
|
|
228
|
+
sources.screens = firstColumnOf(scr.content, 'Screens');
|
|
229
|
+
const req = readIfPresent(node_path_1.default.join(cwd, REQUIREMENTS_FILE));
|
|
230
|
+
if (req.kind === 'unreadable')
|
|
231
|
+
extra.push({ artifact: REQUIREMENTS_FILE, reason: 'artifact_unreadable' });
|
|
232
|
+
if (req.kind === 'ok') {
|
|
233
|
+
const parsed = rollup.parseRequirementsArtifact(req.content);
|
|
234
|
+
sources.requirementIds = parsed.ok ? new Set(parsed.rows.map((r) => r.id)) : 'unparseable';
|
|
235
|
+
}
|
|
236
|
+
const result = checkRoles(read.content, sources);
|
|
237
|
+
result.violations.push(...extra);
|
|
238
|
+
result.ok = result.violations.length === 0;
|
|
239
|
+
return { file: ROLES_FILE, ...result };
|
|
240
|
+
}
|
|
241
|
+
function formatViolation(v) {
|
|
242
|
+
const where = v.line !== undefined ? `:${v.line}` : '';
|
|
243
|
+
const what = v.id !== undefined ? ` (${v.id})` : '';
|
|
244
|
+
return ` - ${v.artifact}${where}: ${v.reason}${what}`;
|
|
245
|
+
}
|
|
246
|
+
// ─── CLI adapter ──────────────────────────────────────────────────────────────
|
|
247
|
+
/** `pp-tools discovery roles-check` (dotted `query discovery.roles-check`). Absent artifact → exit 0, `present: false`. */
|
|
248
|
+
function cmdDiscoveryRolesCheck(cwd, args, raw) {
|
|
249
|
+
if (args.length > 2) {
|
|
250
|
+
error(USAGE, ERROR_REASON.USAGE);
|
|
251
|
+
return; // unreachable
|
|
252
|
+
}
|
|
253
|
+
const result = checkProjectRoles(cwd);
|
|
254
|
+
if (result === null) {
|
|
255
|
+
output({ ok: true, present: false, file: ROLES_FILE }, raw);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
const payload = { ok: result.ok, present: true, file: result.file, roles: result.roles, permissions: result.permissions, screens: result.screens };
|
|
259
|
+
if (!result.ok) {
|
|
260
|
+
output({ ...payload, violations: result.violations.length }, raw);
|
|
261
|
+
error(`discovery roles-check: ${result.violations.length} violation(s):\n` + result.violations.map(formatViolation).join('\n'), ERROR_REASON.DISCOVERY_ROLES_CHECK_FAILED);
|
|
262
|
+
return; // unreachable
|
|
263
|
+
}
|
|
264
|
+
output(payload, raw);
|
|
265
|
+
}
|
|
266
|
+
module.exports = {
|
|
267
|
+
ROLES_FILE,
|
|
268
|
+
ROLES_HEADER,
|
|
269
|
+
PERMISSIONS_HEADER,
|
|
270
|
+
SCREENS_HEADER,
|
|
271
|
+
ROLES_CHECK_FAILED: ERROR_REASON.DISCOVERY_ROLES_CHECK_FAILED,
|
|
272
|
+
stepActors,
|
|
273
|
+
checkRoles,
|
|
274
|
+
checkProjectRoles,
|
|
275
|
+
cmdDiscoveryRolesCheck,
|
|
276
|
+
};
|