@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,264 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Discovery deviations check (Discovery Sprint improvement plan, Batch 3).
|
|
4
|
+
*
|
|
5
|
+
* `.planning/discovery/_deviations.md` is the register of agreed departures
|
|
6
|
+
* from the client's standards: `DEV-nn`, the vault standard deviated from
|
|
7
|
+
* (`[vault: <path>]`), the choice made instead, the reason, whether the choice
|
|
8
|
+
* is easy or hard to reverse (the vault's own distinction — a hard one blocks
|
|
9
|
+
* the affected work until an architect signs), who approves it, the decision
|
|
10
|
+
* that recorded it and its status. Sessions 1 and 4–10 write it as their
|
|
11
|
+
* Standards Applied rows surface a deviation; the charter § 7 copies it; the
|
|
12
|
+
* vault reviewer reads it so an approved deviation is reported as "deviation
|
|
13
|
+
* on file" and never as a violation. This module checks the structure:
|
|
14
|
+
*
|
|
15
|
+
* - the `## Deviations` table exists with the template's columns;
|
|
16
|
+
* - `DEV-` IDs are well-formed, unique, contiguous;
|
|
17
|
+
* - `Standard` cites at least one `[vault: <path>]`, each path safe and,
|
|
18
|
+
* when the vault cache is on disk, present in it;
|
|
19
|
+
* - `Reversible` and `Status` come from their closed vocabularies;
|
|
20
|
+
* - `Approver` is one the Session 1 Approval Matrix names, when that table
|
|
21
|
+
* is on file with rows;
|
|
22
|
+
* - `Decision` is `—` or a decision ID, and an `Approved` or `Rejected` row
|
|
23
|
+
* carries one;
|
|
24
|
+
* - `--require-approved`: a hard-to-reverse deviation that is still
|
|
25
|
+
* `Proposed` fails on its own exit identity, so `/pp-discovery-to-build`
|
|
26
|
+
* can hard-block on it while Sessions 8 and 13 only report.
|
|
27
|
+
*
|
|
28
|
+
* Posture (D-43/D-53): reports at Sessions 8 and 13; hard-blocks at the
|
|
29
|
+
* handoff. Absent artifact → exit 0.
|
|
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
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
40
|
+
const table = require("./discovery-table.cjs");
|
|
41
|
+
// The vault-index leaf owns the vault path rule (14-03 anti-drift: one owner in src/).
|
|
42
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
43
|
+
const vaultIndex = require("./vault-index.cjs");
|
|
44
|
+
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
45
|
+
const DISCOVERY_DIR = '.planning/discovery';
|
|
46
|
+
const DEVIATIONS_FILE = `${DISCOVERY_DIR}/_deviations.md`;
|
|
47
|
+
const SESSION_01_FILE = `${DISCOVERY_DIR}/session-01.md`;
|
|
48
|
+
const DEVIATIONS_SECTION = 'Deviations';
|
|
49
|
+
const DEVIATIONS_HEADER = Object.freeze(['ID', 'Standard', 'Choice', 'Reason', 'Reversible', 'Approver', 'Decision', 'Status']);
|
|
50
|
+
const DEV_RE = /^DEV-(\d{2})$/;
|
|
51
|
+
const REVERSIBLE = Object.freeze(['easy', 'hard']);
|
|
52
|
+
const STATUSES = Object.freeze(['Proposed', 'Approved', 'Rejected', 'Superseded']);
|
|
53
|
+
const DECIDED_STATUSES = Object.freeze(['Approved', 'Rejected']);
|
|
54
|
+
const DECISION_ID_RE = /^D-(\d{2}|P\d{2})-\d+$/;
|
|
55
|
+
const VAULT_CITE_RE = /\[vault:\s*([^\]]+?)\s*\]/g;
|
|
56
|
+
const PENDING = '—';
|
|
57
|
+
const APPROVAL_MATRIX_LABEL = 'Approval Matrix';
|
|
58
|
+
const APPROVER_COLUMN = 'Approver';
|
|
59
|
+
const USAGE = 'Usage: pp-tools discovery deviations-check [--require-approved]';
|
|
60
|
+
function readIfPresent(abs) {
|
|
61
|
+
if (!node_fs_1.default.existsSync(abs))
|
|
62
|
+
return { kind: 'absent' };
|
|
63
|
+
try {
|
|
64
|
+
return { kind: 'ok', content: node_fs_1.default.readFileSync(abs, 'utf-8') };
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return { kind: 'unreadable' };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function sameHeader(actual, expected) {
|
|
71
|
+
return actual.length === expected.length && actual.every((c, i) => c.trim().toLowerCase() === expected[i].toLowerCase());
|
|
72
|
+
}
|
|
73
|
+
function splitList(cell) {
|
|
74
|
+
return cell.split(',').map((t) => t.trim()).filter(Boolean);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* A vault path a deviation may cite: the vault index leaf's path rule (relative,
|
|
78
|
+
* no traversal, no dot or empty segment, display-safe) plus the sessions' own
|
|
79
|
+
* `.md` requirement (`workflows/discovery-sprint/sessions/04-ux-design.md`).
|
|
80
|
+
*/
|
|
81
|
+
function isSafeStandardPath(p) {
|
|
82
|
+
return vaultIndex.isSafeVaultPath(p) && p.endsWith('.md');
|
|
83
|
+
}
|
|
84
|
+
/** The Approver tokens of Session 1's Approval Matrix, by header name; null when absent or empty. */
|
|
85
|
+
function approversFrom(content) {
|
|
86
|
+
const lines = content.split(/\r?\n/);
|
|
87
|
+
const norm = (s) => s.replace(/^#+\s*/, '').replace(/\*\*/g, '').trim();
|
|
88
|
+
const at = lines.findIndex((l) => norm(l) === APPROVAL_MATRIX_LABEL);
|
|
89
|
+
if (at === -1)
|
|
90
|
+
return null;
|
|
91
|
+
const body = [];
|
|
92
|
+
for (let i = at + 1; i < lines.length; i += 1) {
|
|
93
|
+
if (/^#{1,3}\s/.test(lines[i]) || /^\*\*[^*]+\*\*$/.test(lines[i].trim()))
|
|
94
|
+
break;
|
|
95
|
+
body.push({ n: i + 1, text: lines[i] });
|
|
96
|
+
}
|
|
97
|
+
const tables = table.parseTables(body);
|
|
98
|
+
if (tables.length === 0)
|
|
99
|
+
return null;
|
|
100
|
+
const col = tables[0].header.findIndex((h) => h.trim().toLowerCase() === APPROVER_COLUMN.toLowerCase());
|
|
101
|
+
if (col === -1)
|
|
102
|
+
return null;
|
|
103
|
+
const out = new Set();
|
|
104
|
+
for (const r of tables[0].rows)
|
|
105
|
+
for (const t of splitList(r.cells[col] ?? ''))
|
|
106
|
+
out.add(t);
|
|
107
|
+
return out.size === 0 ? null : out;
|
|
108
|
+
}
|
|
109
|
+
/** The vault cache directory on disk, through the vault's own resolver when it loads; null when there is no cache. */
|
|
110
|
+
function vaultCacheDirOf(cwd) {
|
|
111
|
+
let dir = node_path_1.default.join(cwd, '.planning', 'vault');
|
|
112
|
+
try {
|
|
113
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
114
|
+
const vault = require('./vault.cjs');
|
|
115
|
+
if (typeof vault.resolveVaultCacheDir === 'function')
|
|
116
|
+
dir = vault.resolveVaultCacheDir(cwd).dir;
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
/* the local cache path stands */
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
return node_fs_1.default.statSync(dir).isDirectory() ? dir : null;
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// ─── Check ────────────────────────────────────────────────────────────────────
|
|
129
|
+
function checkDeviations(content, sources, requireApproved = false) {
|
|
130
|
+
const violations = [];
|
|
131
|
+
const empty = { deviations: 0, approved: 0, unapproved_hard: 0 };
|
|
132
|
+
const section = table.splitSections(content.split(/\r?\n/), 2).find((s) => s.heading === DEVIATIONS_SECTION);
|
|
133
|
+
if (!section)
|
|
134
|
+
return { ok: false, ...empty, violations: [{ artifact: DEVIATIONS_FILE, reason: 'missing_section', id: DEVIATIONS_SECTION }] };
|
|
135
|
+
const tables = table.parseTables(section.body);
|
|
136
|
+
if (tables.length === 0)
|
|
137
|
+
return { ok: false, ...empty, violations: [{ artifact: DEVIATIONS_FILE, reason: 'missing_section', line: section.line, id: DEVIATIONS_SECTION }] };
|
|
138
|
+
if (!sameHeader(tables[0].header, DEVIATIONS_HEADER)) {
|
|
139
|
+
return { ok: false, ...empty, violations: [{ artifact: DEVIATIONS_FILE, reason: 'bad_table_shape', line: tables[0].line, id: DEVIATIONS_SECTION }] };
|
|
140
|
+
}
|
|
141
|
+
const ids = new Set();
|
|
142
|
+
const seqs = [];
|
|
143
|
+
let approved = 0;
|
|
144
|
+
let unapprovedHard = 0;
|
|
145
|
+
for (const row of tables[0].rows) {
|
|
146
|
+
const [id, standard, choice, reason, reversible, approver, decision, status] = row.cells.map((c) => c.trim());
|
|
147
|
+
const line = row.line;
|
|
148
|
+
const m = DEV_RE.exec(id ?? '');
|
|
149
|
+
if (!m)
|
|
150
|
+
violations.push({ artifact: DEVIATIONS_FILE, reason: 'bad_id', line });
|
|
151
|
+
else if (ids.has(id))
|
|
152
|
+
violations.push({ artifact: DEVIATIONS_FILE, reason: 'duplicate_id', line, id });
|
|
153
|
+
else {
|
|
154
|
+
ids.add(id);
|
|
155
|
+
seqs.push(Number(m[1]));
|
|
156
|
+
}
|
|
157
|
+
const cites = [...(standard ?? '').matchAll(VAULT_CITE_RE)].map((c) => c[1]);
|
|
158
|
+
if (cites.length === 0)
|
|
159
|
+
violations.push({ artifact: DEVIATIONS_FILE, reason: 'bad_standard', line });
|
|
160
|
+
for (const p of cites) {
|
|
161
|
+
if (!isSafeStandardPath(p))
|
|
162
|
+
violations.push({ artifact: DEVIATIONS_FILE, reason: 'bad_standard', line, id: p.slice(0, 80) });
|
|
163
|
+
else if (sources.vaultCacheDir !== null && !node_fs_1.default.existsSync(node_path_1.default.join(sources.vaultCacheDir, p)))
|
|
164
|
+
violations.push({ artifact: DEVIATIONS_FILE, reason: 'unknown_standard', line, id: p });
|
|
165
|
+
}
|
|
166
|
+
if ((choice ?? '') === '')
|
|
167
|
+
violations.push({ artifact: DEVIATIONS_FILE, reason: 'empty_cell', line, id: 'Choice' });
|
|
168
|
+
if ((reason ?? '') === '')
|
|
169
|
+
violations.push({ artifact: DEVIATIONS_FILE, reason: 'empty_cell', line, id: 'Reason' });
|
|
170
|
+
const rev = reversible ?? '';
|
|
171
|
+
if (!REVERSIBLE.includes(rev))
|
|
172
|
+
violations.push({ artifact: DEVIATIONS_FILE, reason: 'bad_reversible', line });
|
|
173
|
+
if ((approver ?? '') === '')
|
|
174
|
+
violations.push({ artifact: DEVIATIONS_FILE, reason: 'empty_cell', line, id: 'Approver' });
|
|
175
|
+
else if (sources.approvers !== null && !sources.approvers.has(approver))
|
|
176
|
+
violations.push({ artifact: DEVIATIONS_FILE, reason: 'unknown_approver', line });
|
|
177
|
+
const dec = decision ?? '';
|
|
178
|
+
const st = status ?? '';
|
|
179
|
+
if (dec !== PENDING && !DECISION_ID_RE.test(dec))
|
|
180
|
+
violations.push({ artifact: DEVIATIONS_FILE, reason: 'bad_decision', line });
|
|
181
|
+
if (!STATUSES.includes(st))
|
|
182
|
+
violations.push({ artifact: DEVIATIONS_FILE, reason: 'bad_status', line });
|
|
183
|
+
else {
|
|
184
|
+
if (DECIDED_STATUSES.includes(st) && dec === PENDING)
|
|
185
|
+
violations.push({ artifact: DEVIATIONS_FILE, reason: 'decision_missing', line, id: m ? id : undefined });
|
|
186
|
+
if (st === 'Approved')
|
|
187
|
+
approved += 1;
|
|
188
|
+
if (st === 'Proposed' && rev === 'hard') {
|
|
189
|
+
unapprovedHard += 1;
|
|
190
|
+
if (requireApproved)
|
|
191
|
+
violations.push({ artifact: DEVIATIONS_FILE, reason: 'hard_deviation_unapproved', line, id: m ? id : undefined });
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
const max = table.boundedSequenceMax(seqs);
|
|
196
|
+
if (max !== null) {
|
|
197
|
+
const have = new Set(seqs);
|
|
198
|
+
for (let n = 1; n <= max; n += 1) {
|
|
199
|
+
if (!have.has(n))
|
|
200
|
+
violations.push({ artifact: DEVIATIONS_FILE, reason: 'sequence_gap', id: `DEV-${String(n).padStart(2, '0')}` });
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return { ok: violations.length === 0, deviations: ids.size, approved, unapproved_hard: unapprovedHard, violations };
|
|
204
|
+
}
|
|
205
|
+
function checkProjectDeviations(cwd, requireApproved = false) {
|
|
206
|
+
const read = readIfPresent(node_path_1.default.join(cwd, DEVIATIONS_FILE));
|
|
207
|
+
if (read.kind === 'absent')
|
|
208
|
+
return null;
|
|
209
|
+
if (read.kind === 'unreadable') {
|
|
210
|
+
return { ok: false, file: DEVIATIONS_FILE, deviations: 0, approved: 0, unapproved_hard: 0, violations: [{ artifact: DEVIATIONS_FILE, reason: 'artifact_unreadable' }] };
|
|
211
|
+
}
|
|
212
|
+
const extra = [];
|
|
213
|
+
const sources = { approvers: null, vaultCacheDir: vaultCacheDirOf(cwd) };
|
|
214
|
+
const s1 = readIfPresent(node_path_1.default.join(cwd, SESSION_01_FILE));
|
|
215
|
+
if (s1.kind === 'unreadable')
|
|
216
|
+
extra.push({ artifact: SESSION_01_FILE, reason: 'artifact_unreadable' });
|
|
217
|
+
if (s1.kind === 'ok')
|
|
218
|
+
sources.approvers = approversFrom(s1.content);
|
|
219
|
+
const result = checkDeviations(read.content, sources, requireApproved);
|
|
220
|
+
result.violations.push(...extra);
|
|
221
|
+
result.ok = result.violations.length === 0;
|
|
222
|
+
return { file: DEVIATIONS_FILE, ...result };
|
|
223
|
+
}
|
|
224
|
+
function formatViolation(v) {
|
|
225
|
+
const where = v.line !== undefined ? `:${v.line}` : '';
|
|
226
|
+
const what = v.id !== undefined ? ` (${v.id})` : '';
|
|
227
|
+
return ` - ${v.artifact}${where}: ${v.reason}${what}`;
|
|
228
|
+
}
|
|
229
|
+
// ─── CLI adapter ──────────────────────────────────────────────────────────────
|
|
230
|
+
/** `pp-tools discovery deviations-check [--require-approved]` (dotted `query discovery.deviations-check`). Absent artifact → exit 0, `present: false`. */
|
|
231
|
+
function cmdDiscoveryDeviationsCheck(cwd, args, raw) {
|
|
232
|
+
const rest = args.slice(2);
|
|
233
|
+
const requireApproved = rest.includes('--require-approved');
|
|
234
|
+
if (rest.some((a) => a !== '--require-approved')) {
|
|
235
|
+
error(USAGE, ERROR_REASON.USAGE);
|
|
236
|
+
return; // unreachable
|
|
237
|
+
}
|
|
238
|
+
const result = checkProjectDeviations(cwd, requireApproved);
|
|
239
|
+
if (result === null) {
|
|
240
|
+
output({ ok: true, present: false, file: DEVIATIONS_FILE }, raw);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
const payload = { ok: result.ok, present: true, file: result.file, deviations: result.deviations, approved: result.approved, unapproved_hard: result.unapproved_hard };
|
|
244
|
+
if (!result.ok) {
|
|
245
|
+
const onlyUnapproved = result.violations.every((v) => v.reason === 'hard_deviation_unapproved');
|
|
246
|
+
output({ ...payload, violations: result.violations.length }, raw);
|
|
247
|
+
error(`discovery deviations-check: ${result.violations.length} violation(s):\n` + result.violations.map(formatViolation).join('\n'), onlyUnapproved ? ERROR_REASON.DISCOVERY_DEVIATIONS_UNAPPROVED : ERROR_REASON.DISCOVERY_DEVIATIONS_CHECK_FAILED);
|
|
248
|
+
return; // unreachable
|
|
249
|
+
}
|
|
250
|
+
output(payload, raw);
|
|
251
|
+
}
|
|
252
|
+
module.exports = {
|
|
253
|
+
DEVIATIONS_FILE,
|
|
254
|
+
DEVIATIONS_HEADER,
|
|
255
|
+
REVERSIBLE,
|
|
256
|
+
STATUSES,
|
|
257
|
+
DEVIATIONS_CHECK_FAILED: ERROR_REASON.DISCOVERY_DEVIATIONS_CHECK_FAILED,
|
|
258
|
+
DEVIATIONS_UNAPPROVED: ERROR_REASON.DISCOVERY_DEVIATIONS_UNAPPROVED,
|
|
259
|
+
isSafeStandardPath,
|
|
260
|
+
approversFrom,
|
|
261
|
+
checkDeviations,
|
|
262
|
+
checkProjectDeviations,
|
|
263
|
+
cmdDiscoveryDeviationsCheck,
|
|
264
|
+
};
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Discovery data-model check (Discovery Sprint improvement plan, Batch 2).
|
|
4
|
+
*
|
|
5
|
+
* `.planning/discovery/_entities.md` is the one data model a build starts
|
|
6
|
+
* from: every entity by name (entities are named, never numbered — Batch 2
|
|
7
|
+
* decision), its identifier, whether it has a status lifecycle, its
|
|
8
|
+
* classification in the vault's four levels, its retention, its owner module
|
|
9
|
+
* and its source; one attribute table per entity; relationships with Mermaid
|
|
10
|
+
* cardinality; lifecycle states and transitions. Session 2 seeds it from the
|
|
11
|
+
* Data Entity Map, Session 3's step matrices name the objects each step
|
|
12
|
+
* touches, Session 5 consolidates it, Session 6 writes classification and
|
|
13
|
+
* retention. This module checks the structure — never the judgement:
|
|
14
|
+
*
|
|
15
|
+
* - the four sections exist, each table has the template's columns;
|
|
16
|
+
* - entity names are PascalCase tokens, unique; `Status lifecycle` and
|
|
17
|
+
* `Classification` are inside their closed vocabularies;
|
|
18
|
+
* - every attribute heading, relationship end and lifecycle row names an
|
|
19
|
+
* entity the `## Entities` table has; `Required` is `yes` or `no`;
|
|
20
|
+
* - every cardinality is a Mermaid `erDiagram` cardinality, so the ERD the
|
|
21
|
+
* handoff draws needs no default;
|
|
22
|
+
* - a lifecycle flag of `yes` has states, and states have a flag;
|
|
23
|
+
* - every data object a step matrix in `_processes.md` touches (`Task [C]`)
|
|
24
|
+
* is an entity;
|
|
25
|
+
* - with `--require-complete`: no `—` is left in Identifier, Status
|
|
26
|
+
* lifecycle, Classification or Retention, and every entity has at least
|
|
27
|
+
* one attribute. Before Session 5 those cells are allowed to be pending.
|
|
28
|
+
*
|
|
29
|
+
* Posture (D-43/D-53): the check reports at Sessions 8 and 13 and hard-blocks
|
|
30
|
+
* at `/pp-discovery-to-build`, where `--require-complete` is added. Absent
|
|
31
|
+
* artifact → exit 0 (an engagement that predates the data model).
|
|
32
|
+
*
|
|
33
|
+
* Content-free contract: output names the artifact, line numbers, entity
|
|
34
|
+
* names (validated tokens), section keys and reason codes — never cell prose.
|
|
35
|
+
* No positional path: convention paths resolve against the working directory.
|
|
36
|
+
*/
|
|
37
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
38
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
39
|
+
};
|
|
40
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
41
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
42
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
43
|
+
const io = require("./io.cjs");
|
|
44
|
+
const { output, error, ERROR_REASON } = io;
|
|
45
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
46
|
+
const table = require("./discovery-table.cjs");
|
|
47
|
+
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
48
|
+
const DISCOVERY_DIR = '.planning/discovery';
|
|
49
|
+
const ENTITIES_FILE = `${DISCOVERY_DIR}/_entities.md`;
|
|
50
|
+
const PROCESSES_FILE = `${DISCOVERY_DIR}/_processes.md`;
|
|
51
|
+
const SECTIONS = Object.freeze({ entities: 'Entities', attributes: 'Attributes', relationships: 'Relationships', lifecycles: 'Lifecycles' });
|
|
52
|
+
const ENTITIES_HEADER = Object.freeze(['Entity', 'Identifier', 'Status lifecycle', 'Classification', 'Retention', 'Owner module', 'Source']);
|
|
53
|
+
const ATTRIBUTES_HEADER = Object.freeze(['Attribute', 'Type', 'Required', 'Bounds', 'Notes']);
|
|
54
|
+
const RELATIONSHIPS_HEADER = Object.freeze(['From', 'Cardinality', 'To', 'Name']);
|
|
55
|
+
const LIFECYCLES_HEADER = Object.freeze(['Entity', 'State', 'Transition', 'Allowed by']);
|
|
56
|
+
/** Entities are named, never numbered: one PascalCase token, cited exactly. */
|
|
57
|
+
const ENTITY_NAME_RE = /^[A-Z][A-Za-z0-9]*$/;
|
|
58
|
+
const PENDING = '—';
|
|
59
|
+
const LIFECYCLE_VALUES = Object.freeze(['none', 'yes', PENDING]);
|
|
60
|
+
/** The vault's four levels (`security/compliance.md` § Information handling). */
|
|
61
|
+
const CLASSIFICATION_LEVELS = Object.freeze(['Public', 'Internal', 'Confidential', 'Restricted']);
|
|
62
|
+
const REQUIRED_VALUES = Object.freeze(['yes', 'no', PENDING]);
|
|
63
|
+
/** Mermaid `erDiagram` cardinality: a left end, an identifying (`--`) or non-identifying (`..`) line, a right end. */
|
|
64
|
+
const CARDINALITY_RE = /^(\|\||\|o|\}o|\}\|)(--|\.\.)(\|\||o\||o\{|\|\{)$/;
|
|
65
|
+
/** A step-matrix data object: `Task [C]`, `Membership [C/R/U]`. */
|
|
66
|
+
const DATA_OBJECT_RE = /^([A-Z][A-Za-z0-9]*) \[([CRUD](?:\/[CRUD])*)\]$/;
|
|
67
|
+
const STEPS_DATA_OBJECTS_COLUMN = 'Data objects';
|
|
68
|
+
const USAGE = 'Usage: pp-tools discovery entity-check [--require-complete]';
|
|
69
|
+
function readIfPresent(abs) {
|
|
70
|
+
if (!node_fs_1.default.existsSync(abs))
|
|
71
|
+
return { kind: 'absent' };
|
|
72
|
+
try {
|
|
73
|
+
return { kind: 'ok', content: node_fs_1.default.readFileSync(abs, 'utf-8') };
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return { kind: 'unreadable' };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function sameHeader(actual, expected) {
|
|
80
|
+
return actual.length === expected.length && actual.every((c, i) => c.trim().toLowerCase() === expected[i].toLowerCase());
|
|
81
|
+
}
|
|
82
|
+
/** The first table of a `##` section, with its header verified; null when the section or table is absent. */
|
|
83
|
+
function sectionTable(sections, heading, expected, violations) {
|
|
84
|
+
const section = sections.find((s) => s.heading === heading);
|
|
85
|
+
if (!section) {
|
|
86
|
+
violations.push({ artifact: ENTITIES_FILE, reason: 'missing_section', id: heading });
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
const tables = table.parseTables(section.body);
|
|
90
|
+
if (tables.length === 0) {
|
|
91
|
+
violations.push({ artifact: ENTITIES_FILE, reason: 'missing_section', line: section.line, id: heading });
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
if (!sameHeader(tables[0].header, expected)) {
|
|
95
|
+
violations.push({ artifact: ENTITIES_FILE, reason: 'bad_table_shape', line: tables[0].line, id: heading });
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
return tables[0];
|
|
99
|
+
}
|
|
100
|
+
// ─── Check ────────────────────────────────────────────────────────────────────
|
|
101
|
+
/** Check `_entities.md` text; `stepObjects` are the `Data objects` cells of `_processes.md` step tables. */
|
|
102
|
+
function checkEntities(content, stepObjects = [], requireComplete = false) {
|
|
103
|
+
const violations = [];
|
|
104
|
+
const lines = content.split(/\r?\n/);
|
|
105
|
+
const sections = table.splitSections(lines, 2);
|
|
106
|
+
// 1 ─ Entities
|
|
107
|
+
const entities = new Map();
|
|
108
|
+
const entitiesTable = sectionTable(sections, SECTIONS.entities, ENTITIES_HEADER, violations);
|
|
109
|
+
if (entitiesTable) {
|
|
110
|
+
for (const row of entitiesTable.rows) {
|
|
111
|
+
const [name, identifier, lifecycle, classification, retention] = row.cells.map((c) => c.trim());
|
|
112
|
+
if (!ENTITY_NAME_RE.test(name ?? '')) {
|
|
113
|
+
violations.push({ artifact: ENTITIES_FILE, reason: 'bad_entity_name', line: row.line });
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (entities.has(name)) {
|
|
117
|
+
violations.push({ artifact: ENTITIES_FILE, reason: 'duplicate_entity', line: row.line, id: name });
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (!LIFECYCLE_VALUES.includes(lifecycle ?? ''))
|
|
121
|
+
violations.push({ artifact: ENTITIES_FILE, reason: 'bad_lifecycle_flag', line: row.line, id: name });
|
|
122
|
+
if ((classification ?? '') !== PENDING && !CLASSIFICATION_LEVELS.includes(classification ?? '')) {
|
|
123
|
+
violations.push({ artifact: ENTITIES_FILE, reason: 'bad_classification', line: row.line, id: name });
|
|
124
|
+
}
|
|
125
|
+
for (const [col, value] of [['Identifier', identifier], ['Retention', retention]]) {
|
|
126
|
+
if ((value ?? '') === '')
|
|
127
|
+
violations.push({ artifact: ENTITIES_FILE, reason: 'empty_cell', line: row.line, id: `${name} · ${col}` });
|
|
128
|
+
}
|
|
129
|
+
if (requireComplete) {
|
|
130
|
+
for (const [col, value] of [['Identifier', identifier], ['Status lifecycle', lifecycle], ['Classification', classification], ['Retention', retention]]) {
|
|
131
|
+
if ((value ?? '') === PENDING)
|
|
132
|
+
violations.push({ artifact: ENTITIES_FILE, reason: 'incomplete_cell', line: row.line, id: `${name} · ${col}` });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
entities.set(name, { name, line: row.line, identifier: identifier ?? '', lifecycle: lifecycle ?? '', classification: classification ?? '', retention: retention ?? '' });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// 2 ─ Attributes: one `### <Entity>` table each
|
|
139
|
+
let attributes = 0;
|
|
140
|
+
const withAttributes = new Set();
|
|
141
|
+
const attributesSection = sections.find((s) => s.heading === SECTIONS.attributes);
|
|
142
|
+
if (!attributesSection) {
|
|
143
|
+
violations.push({ artifact: ENTITIES_FILE, reason: 'missing_section', id: SECTIONS.attributes });
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
const sub = table.splitSections(attributesSection.body.map((l) => l.text), 3);
|
|
147
|
+
for (const s of sub) {
|
|
148
|
+
const name = s.heading.trim();
|
|
149
|
+
const line = attributesSection.body[s.line - 1]?.n ?? attributesSection.line;
|
|
150
|
+
if (!entities.has(name)) {
|
|
151
|
+
violations.push({ artifact: ENTITIES_FILE, reason: 'unknown_entity', line, id: name.slice(0, 60) });
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
const tables = table.parseTables(s.body.map((l, i) => ({ n: attributesSection.body[s.line + i]?.n ?? line, text: l.text })));
|
|
155
|
+
if (tables.length === 0 || !sameHeader(tables[0].header, ATTRIBUTES_HEADER)) {
|
|
156
|
+
violations.push({ artifact: ENTITIES_FILE, reason: 'bad_table_shape', line, id: `${SECTIONS.attributes} · ${name}` });
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (tables[0].rows.length > 0)
|
|
160
|
+
withAttributes.add(name);
|
|
161
|
+
for (const row of tables[0].rows) {
|
|
162
|
+
attributes += 1;
|
|
163
|
+
if ((row.cells[0] ?? '').trim() === '' || (row.cells[1] ?? '').trim() === '') {
|
|
164
|
+
violations.push({ artifact: ENTITIES_FILE, reason: 'empty_cell', line: row.line, id: `${name} · Attribute` });
|
|
165
|
+
}
|
|
166
|
+
if (!REQUIRED_VALUES.includes((row.cells[2] ?? '').trim()))
|
|
167
|
+
violations.push({ artifact: ENTITIES_FILE, reason: 'bad_required', line: row.line, id: name });
|
|
168
|
+
if (requireComplete) {
|
|
169
|
+
for (const [col, idx] of [['Type', 1], ['Required', 2]]) {
|
|
170
|
+
if ((row.cells[idx] ?? '').trim() === PENDING)
|
|
171
|
+
violations.push({ artifact: ENTITIES_FILE, reason: 'incomplete_cell', line: row.line, id: `${name} · ${(row.cells[0] ?? '').trim()} · ${col}` });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (requireComplete) {
|
|
178
|
+
for (const e of entities.values()) {
|
|
179
|
+
if (!withAttributes.has(e.name))
|
|
180
|
+
violations.push({ artifact: ENTITIES_FILE, reason: 'attributes_missing', line: e.line, id: e.name });
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
// 3 ─ Relationships
|
|
184
|
+
let relationships = 0;
|
|
185
|
+
const relTable = sectionTable(sections, SECTIONS.relationships, RELATIONSHIPS_HEADER, violations);
|
|
186
|
+
if (relTable) {
|
|
187
|
+
for (const row of relTable.rows) {
|
|
188
|
+
relationships += 1;
|
|
189
|
+
const [from, cardinality, to] = row.cells.map((c) => c.trim());
|
|
190
|
+
for (const end of [from, to]) {
|
|
191
|
+
if (!entities.has(end ?? ''))
|
|
192
|
+
violations.push({ artifact: ENTITIES_FILE, reason: 'unknown_entity', line: row.line, id: (end ?? '').slice(0, 60) });
|
|
193
|
+
}
|
|
194
|
+
if (!CARDINALITY_RE.test(cardinality ?? ''))
|
|
195
|
+
violations.push({ artifact: ENTITIES_FILE, reason: 'bad_cardinality', line: row.line });
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
// 4 ─ Lifecycles
|
|
199
|
+
let lifecycles = 0;
|
|
200
|
+
const withStates = new Set();
|
|
201
|
+
const lifeTable = sectionTable(sections, SECTIONS.lifecycles, LIFECYCLES_HEADER, violations);
|
|
202
|
+
if (lifeTable) {
|
|
203
|
+
for (const row of lifeTable.rows) {
|
|
204
|
+
lifecycles += 1;
|
|
205
|
+
const name = (row.cells[0] ?? '').trim();
|
|
206
|
+
if (!entities.has(name)) {
|
|
207
|
+
violations.push({ artifact: ENTITIES_FILE, reason: 'unknown_entity', line: row.line, id: name.slice(0, 60) });
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
withStates.add(name);
|
|
211
|
+
if ((row.cells[1] ?? '').trim() === '')
|
|
212
|
+
violations.push({ artifact: ENTITIES_FILE, reason: 'empty_cell', line: row.line, id: `${name} · State` });
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
for (const e of entities.values()) {
|
|
216
|
+
if (e.lifecycle === 'yes' && !withStates.has(e.name))
|
|
217
|
+
violations.push({ artifact: ENTITIES_FILE, reason: 'lifecycle_without_states', line: e.line, id: e.name });
|
|
218
|
+
if (e.lifecycle === 'none' && withStates.has(e.name))
|
|
219
|
+
violations.push({ artifact: ENTITIES_FILE, reason: 'states_without_lifecycle', line: e.line, id: e.name });
|
|
220
|
+
}
|
|
221
|
+
// 5 ─ Every data object a step touches is an entity
|
|
222
|
+
let stepObjectsChecked = 0;
|
|
223
|
+
for (const { line, cell } of stepObjects) {
|
|
224
|
+
if (cell.trim() === PENDING || cell.trim() === '')
|
|
225
|
+
continue;
|
|
226
|
+
for (const token of cell.split(',').map((t) => t.trim()).filter(Boolean)) {
|
|
227
|
+
const m = DATA_OBJECT_RE.exec(token);
|
|
228
|
+
if (!m)
|
|
229
|
+
continue; // a malformed token is process-check's fault to report
|
|
230
|
+
stepObjectsChecked += 1;
|
|
231
|
+
if (!entities.has(m[1]))
|
|
232
|
+
violations.push({ artifact: PROCESSES_FILE, reason: 'unknown_entity', line, id: m[1] });
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return { ok: violations.length === 0, entities: entities.size, attributes, relationships, lifecycles, step_objects_checked: stepObjectsChecked, violations };
|
|
236
|
+
}
|
|
237
|
+
/** The `Data objects` cells of every step table in `_processes.md` § Steps. */
|
|
238
|
+
function stepDataObjects(content) {
|
|
239
|
+
const lines = content.split(/\r?\n/);
|
|
240
|
+
const steps = table.splitSections(lines, 2).find((s) => s.heading === 'Steps');
|
|
241
|
+
if (!steps)
|
|
242
|
+
return [];
|
|
243
|
+
const out = [];
|
|
244
|
+
for (const t of table.parseTables(steps.body)) {
|
|
245
|
+
const col = t.header.findIndex((h) => h.trim().toLowerCase() === STEPS_DATA_OBJECTS_COLUMN.toLowerCase());
|
|
246
|
+
if (col === -1)
|
|
247
|
+
continue;
|
|
248
|
+
for (const row of t.rows)
|
|
249
|
+
out.push({ line: row.line, cell: row.cells[col] ?? '' });
|
|
250
|
+
}
|
|
251
|
+
return out;
|
|
252
|
+
}
|
|
253
|
+
function checkProjectEntities(cwd, requireComplete = false) {
|
|
254
|
+
const read = readIfPresent(node_path_1.default.join(cwd, ENTITIES_FILE));
|
|
255
|
+
if (read.kind === 'absent')
|
|
256
|
+
return null;
|
|
257
|
+
const empty = { entities: 0, attributes: 0, relationships: 0, lifecycles: 0, step_objects_checked: 0 };
|
|
258
|
+
if (read.kind === 'unreadable')
|
|
259
|
+
return { ok: false, file: ENTITIES_FILE, ...empty, violations: [{ artifact: ENTITIES_FILE, reason: 'artifact_unreadable' }] };
|
|
260
|
+
const processes = readIfPresent(node_path_1.default.join(cwd, PROCESSES_FILE));
|
|
261
|
+
const stepObjects = processes.kind === 'ok' ? stepDataObjects(processes.content) : [];
|
|
262
|
+
const result = checkEntities(read.content, stepObjects, requireComplete);
|
|
263
|
+
if (processes.kind === 'unreadable')
|
|
264
|
+
result.violations.push({ artifact: PROCESSES_FILE, reason: 'artifact_unreadable' });
|
|
265
|
+
result.ok = result.violations.length === 0;
|
|
266
|
+
return { file: ENTITIES_FILE, ...result };
|
|
267
|
+
}
|
|
268
|
+
function formatViolation(v) {
|
|
269
|
+
const where = v.line !== undefined ? `:${v.line}` : '';
|
|
270
|
+
const what = v.id !== undefined ? ` (${v.id})` : '';
|
|
271
|
+
return ` - ${v.artifact}${where}: ${v.reason}${what}`;
|
|
272
|
+
}
|
|
273
|
+
// ─── CLI adapter ──────────────────────────────────────────────────────────────
|
|
274
|
+
/**
|
|
275
|
+
* CLI adapter for `pp-tools discovery entity-check [--require-complete]`
|
|
276
|
+
* (dotted form `query discovery.entity-check`). An absent `_entities.md`
|
|
277
|
+
* exits 0 with `{ ok: true, present: false }`. Structural faults exit via
|
|
278
|
+
* `DISCOVERY_ENTITY_CHECK_FAILED`; when every fault is an `incomplete_cell` or
|
|
279
|
+
* `attributes_missing` raised by `--require-complete`, the identity is
|
|
280
|
+
* `DISCOVERY_ENTITY_CHECK_INCOMPLETE`, so a gate can tell "not finished" from
|
|
281
|
+
* "malformed".
|
|
282
|
+
*/
|
|
283
|
+
function cmdDiscoveryEntityCheck(cwd, args, raw) {
|
|
284
|
+
const rest = args.slice(2);
|
|
285
|
+
const requireComplete = rest.includes('--require-complete');
|
|
286
|
+
if (rest.some((a) => a !== '--require-complete')) {
|
|
287
|
+
error(USAGE, ERROR_REASON.USAGE);
|
|
288
|
+
return; // unreachable — error() exits
|
|
289
|
+
}
|
|
290
|
+
const result = checkProjectEntities(cwd, requireComplete);
|
|
291
|
+
if (result === null) {
|
|
292
|
+
output({ ok: true, present: false, file: ENTITIES_FILE }, raw);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
const payload = {
|
|
296
|
+
ok: result.ok, present: true, file: result.file, entities: result.entities, attributes: result.attributes,
|
|
297
|
+
relationships: result.relationships, lifecycles: result.lifecycles, step_objects_checked: result.step_objects_checked,
|
|
298
|
+
};
|
|
299
|
+
if (!result.ok) {
|
|
300
|
+
output({ ...payload, violations: result.violations.length }, raw);
|
|
301
|
+
const onlyIncomplete = result.violations.every((v) => v.reason === 'incomplete_cell' || v.reason === 'attributes_missing');
|
|
302
|
+
error(`discovery entity-check: ${result.violations.length} violation(s):\n` + result.violations.map(formatViolation).join('\n'), onlyIncomplete ? ERROR_REASON.DISCOVERY_ENTITY_CHECK_INCOMPLETE : ERROR_REASON.DISCOVERY_ENTITY_CHECK_FAILED);
|
|
303
|
+
return; // unreachable
|
|
304
|
+
}
|
|
305
|
+
output(payload, raw);
|
|
306
|
+
}
|
|
307
|
+
module.exports = {
|
|
308
|
+
ENTITIES_FILE,
|
|
309
|
+
ENTITIES_HEADER,
|
|
310
|
+
ATTRIBUTES_HEADER,
|
|
311
|
+
RELATIONSHIPS_HEADER,
|
|
312
|
+
LIFECYCLES_HEADER,
|
|
313
|
+
CLASSIFICATION_LEVELS,
|
|
314
|
+
LIFECYCLE_VALUES,
|
|
315
|
+
CARDINALITY_RE,
|
|
316
|
+
DATA_OBJECT_RE,
|
|
317
|
+
ENTITY_CHECK_FAILED: ERROR_REASON.DISCOVERY_ENTITY_CHECK_FAILED,
|
|
318
|
+
ENTITY_CHECK_INCOMPLETE: ERROR_REASON.DISCOVERY_ENTITY_CHECK_INCOMPLETE,
|
|
319
|
+
checkEntities,
|
|
320
|
+
stepDataObjects,
|
|
321
|
+
checkProjectEntities,
|
|
322
|
+
cmdDiscoveryEntityCheck,
|
|
323
|
+
};
|