@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,390 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* `pp-tools discovery element-sweep [--check]` — the prototype element sweep.
|
|
4
|
+
*
|
|
5
|
+
* Lists every functional element on every prototype page, so none can be skipped when deciding
|
|
6
|
+
* whether it must become a requirement. `--check` asserts `_sweep.md` decides each one exactly once.
|
|
7
|
+
* Output is content-free: pages, tags, element ids and requirement IDs, never element text.
|
|
8
|
+
*/
|
|
9
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
10
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
11
|
+
};
|
|
12
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
13
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
14
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
15
|
+
const io = require("./io.cjs");
|
|
16
|
+
const { output, error, ERROR_REASON } = io;
|
|
17
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
18
|
+
const rollup = require("./discovery-rollup.cjs");
|
|
19
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
20
|
+
const table = require("./discovery-table.cjs");
|
|
21
|
+
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
22
|
+
const DISCOVERY_DIR = node_path_1.default.join('.planning', 'discovery');
|
|
23
|
+
const PROTOTYPE_DIR = 'prototype';
|
|
24
|
+
const SWEEP_FILE = '_sweep.md';
|
|
25
|
+
const REQUIREMENTS_FILE = '_requirements.md';
|
|
26
|
+
const ELEMENT_SWEEP_FAILED = ERROR_REASON.DISCOVERY_ELEMENT_SWEEP_FAILED;
|
|
27
|
+
const SWEEP_USAGE = 'Usage: pp-tools discovery element-sweep [--check]';
|
|
28
|
+
const OUTCOMES = Object.freeze(['covered', 'requirement', 'ui', 'drop']);
|
|
29
|
+
const FUNCTIONAL_TAGS = new Set([
|
|
30
|
+
'a', 'button', 'input', 'select', 'textarea', 'form', 'table', 'th', 'dialog', 'details',
|
|
31
|
+
'summary', 'img', 'svg', 'canvas', 'video', 'progress', 'meter', 'output',
|
|
32
|
+
]);
|
|
33
|
+
const FUNCTIONAL_ROLES = new Set([
|
|
34
|
+
'button', 'link', 'tab', 'tablist', 'switch', 'checkbox', 'radio', 'alert', 'status', 'dialog',
|
|
35
|
+
'menu', 'menuitem', 'progressbar', 'slider', 'grid', 'listbox', 'option', 'combobox', 'img',
|
|
36
|
+
]);
|
|
37
|
+
const VOID_TAGS = new Set([
|
|
38
|
+
'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'source', 'track', 'wbr',
|
|
39
|
+
]);
|
|
40
|
+
const RAW_TEXT_TAGS = new Set(['script', 'style', 'noscript']);
|
|
41
|
+
const ID_RE = /^[A-Za-z][A-Za-z0-9_:.-]{0,79}$/;
|
|
42
|
+
const REQ_ID_RE = /^[A-Z][A-Z0-9]*-\d+$/;
|
|
43
|
+
const DECISION_RE = /^D-\d{2}-\d+$/;
|
|
44
|
+
const ELEMENT_RE = /^(\*|[A-Za-z0-9_.-]+\.html?)#([A-Za-z][A-Za-z0-9_:.-]{0,79})$/;
|
|
45
|
+
const ROUND_RE = /^## Sweep (\d{1,2}) — \d{4}-\d{2}-\d{2} — .+$/;
|
|
46
|
+
const TABLE_HEADER = ['Element', 'Outcome', 'Reference'];
|
|
47
|
+
// ─── HTML scanning ────────────────────────────────────────────────────────────
|
|
48
|
+
function lineAt(text, index) {
|
|
49
|
+
let n = 1;
|
|
50
|
+
for (let i = 0; i < index; i += 1)
|
|
51
|
+
if (text.charCodeAt(i) === 10)
|
|
52
|
+
n += 1;
|
|
53
|
+
return n;
|
|
54
|
+
}
|
|
55
|
+
function parseAttrs(src) {
|
|
56
|
+
const attrs = new Map();
|
|
57
|
+
const re = /([^\s=/>"']+)(?:\s*=\s*("[^"]*"|'[^']*'|[^\s>]+))?/g;
|
|
58
|
+
let m;
|
|
59
|
+
while ((m = re.exec(src)) !== null) {
|
|
60
|
+
const raw = m[2] ?? '';
|
|
61
|
+
const value = raw.startsWith('"') || raw.startsWith("'") ? raw.slice(1, -1) : raw;
|
|
62
|
+
attrs.set(m[1].toLowerCase(), value);
|
|
63
|
+
}
|
|
64
|
+
return attrs;
|
|
65
|
+
}
|
|
66
|
+
function isFunctional(tag, attrs) {
|
|
67
|
+
if (attrs.has('data-req'))
|
|
68
|
+
return true;
|
|
69
|
+
if (tag === 'input' && (attrs.get('type') || '').toLowerCase() === 'hidden')
|
|
70
|
+
return false;
|
|
71
|
+
if (FUNCTIONAL_TAGS.has(tag) || tag.includes('-'))
|
|
72
|
+
return true;
|
|
73
|
+
const role = (attrs.get('role') || '').toLowerCase();
|
|
74
|
+
return FUNCTIONAL_ROLES.has(role);
|
|
75
|
+
}
|
|
76
|
+
function screenFrom(tag, attrs) {
|
|
77
|
+
const declared = attrs.get('data-screen');
|
|
78
|
+
if (declared && /^SCR-\d{2}$/.test(declared))
|
|
79
|
+
return declared;
|
|
80
|
+
const tpl = tag === 'template' ? /^scr-(\d{2})$/i.exec(attrs.get('id') || '') : null;
|
|
81
|
+
return tpl ? `SCR-${tpl[1]}` : null;
|
|
82
|
+
}
|
|
83
|
+
function reqIdsOf(value) {
|
|
84
|
+
const cut = value.indexOf(' · ');
|
|
85
|
+
if (cut <= 0)
|
|
86
|
+
return null;
|
|
87
|
+
const ids = value.slice(0, cut).split(',').map((s) => s.trim());
|
|
88
|
+
return ids.every((id) => REQ_ID_RE.test(id)) ? ids : null;
|
|
89
|
+
}
|
|
90
|
+
/** Walk one page's tags, tracking the enclosing screen; script and style bodies are skipped. */
|
|
91
|
+
function scanPage(page, html, violations) {
|
|
92
|
+
const text = html.replace(/<!--[\s\S]*?-->/g, (c) => c.replace(/[^\n]/g, ' '));
|
|
93
|
+
const stack = [];
|
|
94
|
+
const found = [];
|
|
95
|
+
const tagRe = /<(\/?)([A-Za-z][A-Za-z0-9-]*)((?:[^>"']|"[^"]*"|'[^']*')*)>/g;
|
|
96
|
+
let m;
|
|
97
|
+
while ((m = tagRe.exec(text)) !== null) {
|
|
98
|
+
const closing = m[1] === '/';
|
|
99
|
+
const tag = m[2].toLowerCase();
|
|
100
|
+
if (closing) {
|
|
101
|
+
const at = stack.map((s) => s.tag).lastIndexOf(tag);
|
|
102
|
+
if (at !== -1)
|
|
103
|
+
stack.length = at;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
const attrs = parseAttrs(m[3]);
|
|
107
|
+
const parentScreen = stack.length ? stack[stack.length - 1].screen : null;
|
|
108
|
+
const screen = screenFrom(tag, attrs) ?? parentScreen;
|
|
109
|
+
const line = lineAt(text, m.index);
|
|
110
|
+
if (isFunctional(tag, attrs)) {
|
|
111
|
+
const id = attrs.get('id') ?? null;
|
|
112
|
+
let reqs = [];
|
|
113
|
+
if (attrs.has('data-req')) {
|
|
114
|
+
const parsed = reqIdsOf(attrs.get('data-req') || '');
|
|
115
|
+
if (parsed === null)
|
|
116
|
+
violations.push({ artifact: `${PROTOTYPE_DIR}/${page}`, reason: 'bad_tag', line, tag });
|
|
117
|
+
else
|
|
118
|
+
reqs = parsed;
|
|
119
|
+
}
|
|
120
|
+
if (id === null)
|
|
121
|
+
violations.push({ artifact: `${PROTOTYPE_DIR}/${page}`, reason: 'element_without_id', line, tag });
|
|
122
|
+
else if (!ID_RE.test(id))
|
|
123
|
+
violations.push({ artifact: `${PROTOTYPE_DIR}/${page}`, reason: 'bad_element_id', line, tag });
|
|
124
|
+
else
|
|
125
|
+
found.push({ id, tag, screen, reqs, line });
|
|
126
|
+
}
|
|
127
|
+
// A script holding a template (`text/x-template` and kin) is markup the kit renders; sweep it.
|
|
128
|
+
const scriptType = tag === 'script' ? (attrs.get('type') || '').toLowerCase() : '';
|
|
129
|
+
const isTemplate = tag === 'script' && scriptType !== '' && !/javascript|ecmascript|^module$|json/.test(scriptType);
|
|
130
|
+
if (RAW_TEXT_TAGS.has(tag) && !isTemplate) {
|
|
131
|
+
const end = text.toLowerCase().indexOf(`</${tag}`, tagRe.lastIndex);
|
|
132
|
+
tagRe.lastIndex = end === -1 ? text.length : end;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (!VOID_TAGS.has(tag) && !m[3].trim().endsWith('/'))
|
|
136
|
+
stack.push({ tag, screen });
|
|
137
|
+
}
|
|
138
|
+
return found;
|
|
139
|
+
}
|
|
140
|
+
/** The current prototype's pages: the top level only, since earlier versions are archived in subfolders. */
|
|
141
|
+
function listPages(dir) {
|
|
142
|
+
let entries;
|
|
143
|
+
try {
|
|
144
|
+
entries = node_fs_1.default.readdirSync(dir, { withFileTypes: true });
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return [];
|
|
148
|
+
}
|
|
149
|
+
return entries.filter((e) => e.isFile() && /\.html?$/i.test(e.name)).map((e) => e.name).sort();
|
|
150
|
+
}
|
|
151
|
+
/** Every functional element across the prototype; an id on several pages is one shared element. */
|
|
152
|
+
function collectElements(protoDir, violations) {
|
|
153
|
+
const pages = listPages(protoDir);
|
|
154
|
+
const byId = new Map();
|
|
155
|
+
for (const page of pages) {
|
|
156
|
+
let html = '';
|
|
157
|
+
try {
|
|
158
|
+
html = node_fs_1.default.readFileSync(node_path_1.default.join(protoDir, page), 'utf-8');
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
violations.push({ artifact: `${PROTOTYPE_DIR}/${page}`, reason: 'artifact_unreadable' });
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
const seen = new Set();
|
|
165
|
+
for (const el of scanPage(page, html, violations)) {
|
|
166
|
+
if (seen.has(el.id)) {
|
|
167
|
+
violations.push({ artifact: `${PROTOTYPE_DIR}/${page}`, reason: 'duplicate_element_id', line: el.line, id: el.id });
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
seen.add(el.id);
|
|
171
|
+
const entry = byId.get(el.id);
|
|
172
|
+
if (entry) {
|
|
173
|
+
entry.pages.push(page);
|
|
174
|
+
el.reqs.forEach((r) => entry.reqs.add(r));
|
|
175
|
+
if (entry.screen !== el.screen)
|
|
176
|
+
entry.screen = null;
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
byId.set(el.id, { pages: [page], tag: el.tag, screen: el.screen, reqs: new Set(el.reqs) });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const elements = [...byId.entries()].map(([id, e]) => ({
|
|
184
|
+
element: `${e.pages.length > 1 ? '*' : e.pages[0]}#${id}`,
|
|
185
|
+
pages: e.pages,
|
|
186
|
+
tag: e.tag,
|
|
187
|
+
screen: e.screen,
|
|
188
|
+
reqs: [...e.reqs].sort(),
|
|
189
|
+
}));
|
|
190
|
+
elements.sort((a, b) => (a.element < b.element ? -1 : a.element > b.element ? 1 : 0));
|
|
191
|
+
return { pages: pages.length, elements };
|
|
192
|
+
}
|
|
193
|
+
// ─── _sweep.md ────────────────────────────────────────────────────────────────
|
|
194
|
+
/** The rows of the highest-numbered round; every round's heading and sequence is checked. */
|
|
195
|
+
function parseSweep(content, violations) {
|
|
196
|
+
const lines = content.split(/\r?\n/);
|
|
197
|
+
const rounds = [];
|
|
198
|
+
let current = null;
|
|
199
|
+
lines.forEach((raw, i) => {
|
|
200
|
+
const line = raw.trim();
|
|
201
|
+
if (line.startsWith('## ')) {
|
|
202
|
+
const m = ROUND_RE.exec(line);
|
|
203
|
+
if (!m) {
|
|
204
|
+
violations.push({ artifact: SWEEP_FILE, reason: 'bad_round_heading', line: i + 1 });
|
|
205
|
+
current = null;
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
current = { n: Number(m[1]), rows: [] };
|
|
209
|
+
rounds.push(current);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if (!current || !line.startsWith('|') || table.SEPARATOR_ROW_RE.test(line))
|
|
213
|
+
return;
|
|
214
|
+
const cells = table.splitTableCells(line);
|
|
215
|
+
if (cells.join('|') === TABLE_HEADER.join('|'))
|
|
216
|
+
return;
|
|
217
|
+
if (cells.length !== 3) {
|
|
218
|
+
violations.push({ artifact: SWEEP_FILE, reason: 'bad_row', line: i + 1 });
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
current.rows.push({
|
|
222
|
+
element: cells[0],
|
|
223
|
+
outcome: cells[1],
|
|
224
|
+
refs: cells[2].split(',').map((s) => s.trim()).filter(Boolean),
|
|
225
|
+
line: i + 1,
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
const numbers = rounds.map((r) => r.n);
|
|
229
|
+
const max = table.boundedSequenceMax(numbers);
|
|
230
|
+
if (max === null) {
|
|
231
|
+
violations.push({ artifact: SWEEP_FILE, reason: 'sequence_gap' });
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
for (let n = 1; n <= max; n += 1) {
|
|
235
|
+
const count = numbers.filter((x) => x === n).length;
|
|
236
|
+
if (count === 0)
|
|
237
|
+
violations.push({ artifact: SWEEP_FILE, reason: 'sequence_gap', round: n });
|
|
238
|
+
if (count > 1)
|
|
239
|
+
violations.push({ artifact: SWEEP_FILE, reason: 'duplicate_round', round: n });
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (rounds.length === 0)
|
|
243
|
+
return { round: null, rows: [] };
|
|
244
|
+
const latest = rounds.reduce((a, b) => (b.n > a.n ? b : a));
|
|
245
|
+
return { round: latest.n, rows: latest.rows };
|
|
246
|
+
}
|
|
247
|
+
function loadRequirements(dir, violations) {
|
|
248
|
+
let content;
|
|
249
|
+
try {
|
|
250
|
+
content = node_fs_1.default.readFileSync(node_path_1.default.join(dir, REQUIREMENTS_FILE), 'utf-8');
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
const parsed = rollup.parseRequirementsArtifact(content);
|
|
256
|
+
if (!parsed.ok) {
|
|
257
|
+
violations.push({ artifact: REQUIREMENTS_FILE, reason: 'requirements_unparseable' });
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
return new Map(parsed.rows.map((r) => [r.id, r.disposition]));
|
|
261
|
+
}
|
|
262
|
+
function checkRows(rows, elements, reqs, violations) {
|
|
263
|
+
const byElement = new Map(elements.map((e) => [e.element, e]));
|
|
264
|
+
const decided = new Set();
|
|
265
|
+
for (const row of rows) {
|
|
266
|
+
const at = { artifact: SWEEP_FILE, line: row.line };
|
|
267
|
+
if (!ELEMENT_RE.test(row.element)) {
|
|
268
|
+
violations.push({ ...at, reason: 'bad_element' });
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
const el = byElement.get(row.element);
|
|
272
|
+
if (!el) {
|
|
273
|
+
violations.push({ ...at, reason: 'stale_element', element: row.element });
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
if (decided.has(row.element)) {
|
|
277
|
+
violations.push({ ...at, reason: 'duplicate_element', element: row.element });
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
decided.add(row.element);
|
|
281
|
+
if (!OUTCOMES.includes(row.outcome)) {
|
|
282
|
+
violations.push({ ...at, reason: 'bad_outcome', element: row.element });
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
if (row.refs.length === 0) {
|
|
286
|
+
violations.push({ ...at, reason: 'missing_reference', element: row.element });
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
if (row.outcome === 'ui' || row.outcome === 'drop') {
|
|
290
|
+
if (row.refs.length !== 1 || !DECISION_RE.test(row.refs[0]))
|
|
291
|
+
violations.push({ ...at, reason: 'bad_reference', element: row.element });
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
for (const id of row.refs) {
|
|
295
|
+
if (!REQ_ID_RE.test(id)) {
|
|
296
|
+
violations.push({ ...at, reason: 'bad_reference', element: row.element });
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
const disposition = reqs?.get(id);
|
|
300
|
+
if (reqs && disposition === undefined)
|
|
301
|
+
violations.push({ ...at, reason: 'unknown_requirement', element: row.element, id });
|
|
302
|
+
else if (disposition && disposition.startsWith('Dropped'))
|
|
303
|
+
violations.push({ ...at, reason: 'dropped_requirement', element: row.element, id });
|
|
304
|
+
if (!el.reqs.includes(id))
|
|
305
|
+
violations.push({ ...at, reason: 'reference_not_tagged', element: row.element, id });
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return elements.map((e) => e.element).filter((e) => !decided.has(e));
|
|
309
|
+
}
|
|
310
|
+
// ─── Entry points ─────────────────────────────────────────────────────────────
|
|
311
|
+
function runElementSweep(cwd, check) {
|
|
312
|
+
const dir = node_path_1.default.join(cwd, DISCOVERY_DIR);
|
|
313
|
+
const protoDir = node_path_1.default.join(dir, PROTOTYPE_DIR);
|
|
314
|
+
const violations = [];
|
|
315
|
+
if (!node_fs_1.default.existsSync(protoDir)) {
|
|
316
|
+
return { ok: true, pages: 0, elements: [], undecided: [], round: null, violations };
|
|
317
|
+
}
|
|
318
|
+
const { pages, elements } = collectElements(protoDir, violations);
|
|
319
|
+
const reqs = loadRequirements(dir, violations);
|
|
320
|
+
if (reqs) {
|
|
321
|
+
for (const el of elements) {
|
|
322
|
+
for (const id of el.reqs) {
|
|
323
|
+
if (!reqs.has(id))
|
|
324
|
+
violations.push({ artifact: PROTOTYPE_DIR, reason: 'unknown_requirement', element: el.element, id });
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
let undecided = [];
|
|
329
|
+
let round = null;
|
|
330
|
+
if (check && pages > 0) {
|
|
331
|
+
let content = null;
|
|
332
|
+
try {
|
|
333
|
+
content = node_fs_1.default.readFileSync(node_path_1.default.join(dir, SWEEP_FILE), 'utf-8');
|
|
334
|
+
}
|
|
335
|
+
catch {
|
|
336
|
+
violations.push({ artifact: SWEEP_FILE, reason: 'sweep_missing' });
|
|
337
|
+
}
|
|
338
|
+
if (content !== null) {
|
|
339
|
+
const parsed = parseSweep(content, violations);
|
|
340
|
+
round = parsed.round;
|
|
341
|
+
if (round === null)
|
|
342
|
+
violations.push({ artifact: SWEEP_FILE, reason: 'no_round' });
|
|
343
|
+
undecided = checkRows(parsed.rows, elements, reqs, violations);
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
undecided = elements.map((e) => e.element);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return { ok: violations.length === 0 && undecided.length === 0, pages, elements, undecided, round, violations };
|
|
350
|
+
}
|
|
351
|
+
function formatViolation(v) {
|
|
352
|
+
const where = v.line === undefined ? v.artifact : `${v.artifact}:${v.line}`;
|
|
353
|
+
const detail = [v.element, v.tag && `<${v.tag}>`, v.id, v.round !== undefined && `round ${v.round}`].filter(Boolean).join(', ');
|
|
354
|
+
return detail ? ` ${where}: ${v.reason} (${detail})` : ` ${where}: ${v.reason}`;
|
|
355
|
+
}
|
|
356
|
+
function cmdDiscoveryElementSweep(cwd, args, raw) {
|
|
357
|
+
const rest = args.slice(2);
|
|
358
|
+
if (rest.some((a) => a !== '--check')) {
|
|
359
|
+
error(SWEEP_USAGE, ERROR_REASON.USAGE);
|
|
360
|
+
return; // unreachable — error() exits
|
|
361
|
+
}
|
|
362
|
+
const check = rest.includes('--check');
|
|
363
|
+
const result = runElementSweep(cwd, check);
|
|
364
|
+
const payload = {
|
|
365
|
+
ok: result.ok,
|
|
366
|
+
pages: result.pages,
|
|
367
|
+
elements: result.elements,
|
|
368
|
+
round: result.round,
|
|
369
|
+
undecided: result.undecided,
|
|
370
|
+
violations: result.violations.length,
|
|
371
|
+
};
|
|
372
|
+
if (!result.ok) {
|
|
373
|
+
output(payload, raw);
|
|
374
|
+
const lines = result.violations.map(formatViolation);
|
|
375
|
+
result.undecided.forEach((e) => lines.push(` ${SWEEP_FILE}: undecided_element (${e})`));
|
|
376
|
+
error(`discovery element-sweep: ${lines.length} fault(s):\n${lines.join('\n')}`, ELEMENT_SWEEP_FAILED);
|
|
377
|
+
return; // unreachable — error() exits
|
|
378
|
+
}
|
|
379
|
+
output(payload, raw);
|
|
380
|
+
}
|
|
381
|
+
module.exports = {
|
|
382
|
+
OUTCOMES,
|
|
383
|
+
SWEEP_FILE,
|
|
384
|
+
ELEMENT_SWEEP_FAILED,
|
|
385
|
+
scanPage,
|
|
386
|
+
collectElements,
|
|
387
|
+
parseSweep,
|
|
388
|
+
runElementSweep,
|
|
389
|
+
cmdDiscoveryElementSweep,
|
|
390
|
+
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Shared markdown-table primitives for the discovery engines (discovery-v2
|
|
4
|
+
* Phase 4, D-60).
|
|
5
|
+
*
|
|
6
|
+
* This is a LEAF: it imports nothing but what TypeScript gives it, has no
|
|
7
|
+
* `node:` dependencies of its own, and names no artifact, ID family or reason
|
|
8
|
+
* code. It holds the structural primitives BOTH register engines need: turning
|
|
9
|
+
* one markdown pipe-table line into cells, recognising a separator row, and
|
|
10
|
+
* taking the bounded maximum of a parsed sequence.
|
|
11
|
+
*
|
|
12
|
+
* The charter was widened from "table primitives" to "register-structure
|
|
13
|
+
* primitives" deliberately, to close AR-04-02. `boundedSequenceMax` had lived
|
|
14
|
+
* privately in `discovery-screens.cts`, so the sibling engine kept its own
|
|
15
|
+
* unbounded `Math.max(...seqs)` and the two drifted the moment the screens
|
|
16
|
+
* engine was hardened. A bound that exists in two places is two things to
|
|
17
|
+
* maintain and one of them is always the stale one; there is now exactly one.
|
|
18
|
+
*
|
|
19
|
+
* Consumers: `discovery-research-track.cts` (which owned `splitRegisterCells`
|
|
20
|
+
* before this module existed) and `discovery-screens.cts`. Both read artifacts
|
|
21
|
+
* with declared FREE-TEXT cells — `Item` and `Data to obtain` for the research
|
|
22
|
+
* track; `Purpose`, the deferral reason and the feedback text for the screen
|
|
23
|
+
* inventory — and free text contains pipes.
|
|
24
|
+
*
|
|
25
|
+
* `discovery-rollup.cts` deliberately keeps its own naive `splitCells` and its
|
|
26
|
+
* own separator constant. Re-pointing a fail-loud parser at a different
|
|
27
|
+
* splitter is a behaviour change to the `_requirements.md` grammar — an author
|
|
28
|
+
* whose row today fails to parse would start parsing, or vice versa — and no
|
|
29
|
+
* Phase 4 decision authorises that. The duplication is the conservative half of
|
|
30
|
+
* the trade the phase deliberately made: share the splitter, leave both
|
|
31
|
+
* existing row collectors and rollup's separator constant exactly where they
|
|
32
|
+
* are. See discovery-v2 Phase 4 (D-60) and `04-PATTERNS.md`.
|
|
33
|
+
*/
|
|
34
|
+
/**
|
|
35
|
+
* Split a pipe-table line into trimmed cells (outer pipes stripped), honouring
|
|
36
|
+
* the markdown `\|` escape.
|
|
37
|
+
*
|
|
38
|
+
* Moved verbatim in behaviour from `discovery-research-track.cts`'s
|
|
39
|
+
* `splitRegisterCells`. A naive `.split('|')` shifts every subsequent column
|
|
40
|
+
* once a free-text cell contains a pipe, landing prose in an enum slot where it
|
|
41
|
+
* is both echoed and reported as a cascade of false faults (`03-REVIEW.md`
|
|
42
|
+
* § CR-02). The correctly-escaped `\|` is the author's documented workaround.
|
|
43
|
+
*
|
|
44
|
+
* Regex rationale (T-03-02 discipline, accepted at AR-03-02 on exactly this
|
|
45
|
+
* shape): the lookbehind is FIXED-WIDTH (one character), quantifier-free and
|
|
46
|
+
* alternation-free, and it is applied to a SINGLE already-split line — never
|
|
47
|
+
* unanchored across whole-file untrusted content. It cannot backtrack
|
|
48
|
+
* catastrophically, so it adds no denial-of-service surface.
|
|
49
|
+
*/
|
|
50
|
+
function splitTableCells(line) {
|
|
51
|
+
const trimmed = line.trim().replace(/^\|/, '').replace(/(?<!\\)\|$/, '');
|
|
52
|
+
return trimmed.split(/(?<!\\)\|/).map(c => c.replace(/\\\|/g, '|').trim());
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* A markdown table separator row (`|----|----|`, alignment colons allowed).
|
|
56
|
+
*
|
|
57
|
+
* The pattern is copied from `discovery-rollup.cts`; rollup's own constant is
|
|
58
|
+
* NOT re-pointed at this one, for the reason given in the module comment above.
|
|
59
|
+
*/
|
|
60
|
+
const SEPARATOR_ROW_RE = /^\s*\|[\s:|-]+\|\s*$/;
|
|
61
|
+
/**
|
|
62
|
+
* The widest span a sequence check will ever walk. Every discovery ID family is
|
|
63
|
+
* two digits, so a span beyond this is not a real inventory — it is a malformed
|
|
64
|
+
* number that reached the sequence loop.
|
|
65
|
+
*/
|
|
66
|
+
const MAX_SEQUENCE_SPAN = 99;
|
|
67
|
+
/**
|
|
68
|
+
* The highest number in `seqs`, or `null` when it exceeds `MAX_SEQUENCE_SPAN`.
|
|
69
|
+
*
|
|
70
|
+
* Two separate hazards are closed here, deliberately in ONE place that every
|
|
71
|
+
* sequence check in both engines calls, so neither hazard can be repaired for
|
|
72
|
+
* one register family and left open for another:
|
|
73
|
+
*
|
|
74
|
+
* - `Math.max(...seqs)` spreads the array into an argument list and throws
|
|
75
|
+
* `RangeError: Maximum call stack size exceeded` on a few hundred thousand
|
|
76
|
+
* entries. The `reduce` has no such limit.
|
|
77
|
+
* - Every caller's gap loop allocates one violation per MISSING number, so an
|
|
78
|
+
* unbounded max turns one mistyped heading into an out-of-memory abort. The
|
|
79
|
+
* ceiling is enforced HERE rather than only in the callers' grammars, so a
|
|
80
|
+
* later widening of any of those regexes cannot silently re-open the hole.
|
|
81
|
+
*
|
|
82
|
+
* EXPORTED SO IT CAN BE FALSIFIED. Both hazards are secondary floors: the
|
|
83
|
+
* primary guards are the two-digit ID grammars, which make the `null` arm
|
|
84
|
+
* unreachable end to end. While this fold was private, neutering its ceiling
|
|
85
|
+
* and restoring the spread each left the whole suite green (04-SECURITY M4/M5)
|
|
86
|
+
* — real in the code, invisible to the suite. Exporting it is what gives the
|
|
87
|
+
* pins something to reach.
|
|
88
|
+
*/
|
|
89
|
+
function boundedSequenceMax(seqs) {
|
|
90
|
+
const max = seqs.reduce((a, b) => (b > a ? b : a), 0);
|
|
91
|
+
return max > MAX_SEQUENCE_SPAN ? null : max;
|
|
92
|
+
}
|
|
93
|
+
module.exports = {
|
|
94
|
+
splitTableCells,
|
|
95
|
+
SEPARATOR_ROW_RE,
|
|
96
|
+
MAX_SEQUENCE_SPAN,
|
|
97
|
+
boundedSequenceMax,
|
|
98
|
+
};
|