@pareto4/pp-core 1.11.0 → 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.
Files changed (256) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +31 -0
  3. package/agents/pp-code-fixer.md +1 -1
  4. package/agents/pp-debug-session-manager.md +1 -1
  5. package/agents/pp-debugger.md +1 -1
  6. package/agents/pp-executor.md +20 -10
  7. package/agents/pp-intel-updater.md +1 -1
  8. package/agents/pp-phase-researcher.md +1 -1
  9. package/agents/pp-plan-checker.md +5 -5
  10. package/agents/pp-planner.md +10 -24
  11. package/agents/pp-project-researcher.md +1 -1
  12. package/agents/pp-research-synthesizer.md +1 -1
  13. package/agents/pp-ui-researcher.md +1 -1
  14. package/agents/pp-verifier.md +8 -4
  15. package/bin/install.js +429 -82
  16. package/commands/pp-autonomous.md +8 -6
  17. package/commands/pp-discovery-to-build.md +1 -1
  18. package/commands/pp-discovery.md +3 -2
  19. package/commands/pp-discuss-phase.md +2 -2
  20. package/commands/pp-execute-phase.md +3 -1
  21. package/commands/pp-graphify.md +5 -5
  22. package/commands/pp-import.md +1 -1
  23. package/commands/pp-map-codebase.md +1 -1
  24. package/commands/pp-ns-project.md +2 -3
  25. package/commands/pp-ns-workflow.md +1 -1
  26. package/commands/pp-plan-phase.md +2 -1
  27. package/commands/pp-verify-work.md +2 -1
  28. package/gemini-extension.json +1 -1
  29. package/hooks/dist/pp-cursor-session-start.js +2 -2
  30. package/hooks/dist/pp-session-state.sh +1 -1
  31. package/hooks/dist/pp-statusline.js +21 -2
  32. package/hooks/pp-cursor-session-start.js +2 -2
  33. package/hooks/pp-session-state.sh +1 -1
  34. package/hooks/pp-statusline.js +21 -2
  35. package/package.json +18 -5
  36. package/pareto-skills/pp-project-setup/SKILL.md +5 -63
  37. package/pp-core/bin/detect-build-commands.sh +182 -0
  38. package/pp-core/bin/lib/capability-registry.cjs +65 -7
  39. package/pp-core/bin/lib/check-command-router.cjs +79 -38
  40. package/pp-core/bin/lib/clusters.cjs +2 -3
  41. package/pp-core/bin/lib/command-aliases.cjs +36 -0
  42. package/pp-core/bin/lib/command-arg-projection.cjs +99 -1
  43. package/pp-core/bin/lib/commands.cjs +21 -6
  44. package/pp-core/bin/lib/compose-bindings.cjs +128 -15
  45. package/pp-core/bin/lib/config-schema.cjs +16 -0
  46. package/pp-core/bin/lib/config.cjs +142 -27
  47. package/pp-core/bin/lib/core-utils.cjs +43 -7
  48. package/pp-core/bin/lib/discovery-input-check.cjs +218 -0
  49. package/pp-core/bin/lib/discovery-rebaseline.cjs +1372 -0
  50. package/pp-core/bin/lib/discovery-research-track.cjs +979 -0
  51. package/pp-core/bin/lib/discovery-rollup.cjs +476 -0
  52. package/pp-core/bin/lib/discovery-screens.cjs +1995 -0
  53. package/pp-core/bin/lib/discovery-sweep.cjs +390 -0
  54. package/pp-core/bin/lib/discovery-table.cjs +98 -0
  55. package/pp-core/bin/lib/dispatch-plan.cjs +472 -0
  56. package/pp-core/bin/lib/frontmatter.cjs +17 -2
  57. package/pp-core/bin/lib/init-command-router.cjs +8 -5
  58. package/pp-core/bin/lib/init-contract.cjs +213 -33
  59. package/pp-core/bin/lib/init-interactive.cjs +20 -1
  60. package/pp-core/bin/lib/init.cjs +124 -134
  61. package/pp-core/bin/lib/install-profiles.cjs +5 -0
  62. package/pp-core/bin/lib/install-run.cjs +536 -0
  63. package/pp-core/bin/lib/io.cjs +15 -0
  64. package/pp-core/bin/lib/loop-resolver.cjs +24 -0
  65. package/pp-core/bin/lib/milestone.cjs +345 -48
  66. package/pp-core/bin/lib/phase-command-router.cjs +23 -1
  67. package/pp-core/bin/lib/phase-locator.cjs +15 -6
  68. package/pp-core/bin/lib/phase-scope.cjs +91 -0
  69. package/pp-core/bin/lib/phase-tier.cjs +467 -0
  70. package/pp-core/bin/lib/phase.cjs +247 -21
  71. package/pp-core/bin/lib/phases-command-router.cjs +2 -0
  72. package/pp-core/bin/lib/plan-graph.cjs +368 -0
  73. package/pp-core/bin/lib/planning-workspace.cjs +53 -0
  74. package/pp-core/bin/lib/platform-bindings.cjs +32 -2
  75. package/pp-core/bin/lib/profile-output.cjs +11 -3
  76. package/pp-core/bin/lib/prohibition-enforcement.cjs +112 -0
  77. package/pp-core/bin/lib/roadmap-parser.cjs +366 -99
  78. package/pp-core/bin/lib/roadmap.cjs +141 -39
  79. package/pp-core/bin/lib/runtime-hooks-surface.cjs +1 -1
  80. package/pp-core/bin/lib/shell-command-projection.cjs +24 -0
  81. package/pp-core/bin/lib/state-command-router.cjs +82 -13
  82. package/pp-core/bin/lib/state-document.cjs +124 -7
  83. package/pp-core/bin/lib/state.cjs +784 -257
  84. package/pp-core/bin/lib/template.cjs +22 -6
  85. package/pp-core/bin/lib/vault-command-router.cjs +57 -6
  86. package/pp-core/bin/lib/vault-practices.cjs +6 -36
  87. package/pp-core/bin/lib/vault-write-back.cjs +20 -4
  88. package/pp-core/bin/lib/vault.cjs +555 -23
  89. package/pp-core/bin/lib/verify.cjs +53 -31
  90. package/pp-core/bin/lib/workstream.cjs +1 -1
  91. package/pp-core/bin/lib/worktree-safety.cjs +26 -15
  92. package/pp-core/bin/portable-timeout.sh +89 -0
  93. package/pp-core/bin/pp-tools.cjs +158 -8
  94. package/pp-core/bin/shared/config-defaults.manifest.json +7 -0
  95. package/pp-core/bin/shared/config-schema.manifest.json +3 -0
  96. package/pp-core/references/autonomous-best-effort.md +461 -0
  97. package/pp-core/references/autonomous-lifecycle.md +146 -0
  98. package/pp-core/references/autonomous-smart-discuss.md +16 -12
  99. package/pp-core/references/checkpoints.md +1 -1
  100. package/pp-core/references/discovery-id-conventions.md +202 -0
  101. package/pp-core/references/discovery-rebaseline.md +254 -0
  102. package/pp-core/references/discovery-research-track.md +283 -0
  103. package/pp-core/references/discovery-traceability.md +400 -0
  104. package/pp-core/references/loop-hook-dispatch.md +7 -0
  105. package/pp-core/references/mandatory-initial-read.md +1 -1
  106. package/pp-core/references/planner-gap-closure.md +1 -0
  107. package/pp-core/references/planner-human-verify-mode.md +1 -1
  108. package/pp-core/references/planner-load-graph-context.md +1 -1
  109. package/pp-core/references/planner-plan-graph.md +66 -0
  110. package/pp-core/references/planner-tdd.md +1 -0
  111. package/pp-core/references/planning-config.md +8 -0
  112. package/pp-core/references/sketch-tooling.md +4 -0
  113. package/pp-core/references/tdd.md +1 -0
  114. package/pp-core/templates/claude-md.md +1 -1
  115. package/pp-core/templates/phase-prompt.md +6 -2
  116. package/pp-core/templates/prototype-review/review.js +303 -0
  117. package/pp-core/templates/state.md +6 -0
  118. package/pp-core/workflows/_runtime-launcher.snippet.sh +1 -1
  119. package/pp-core/workflows/add-backlog.md +1 -1
  120. package/pp-core/workflows/add-phase.md +1 -1
  121. package/pp-core/workflows/add-tests.md +1 -1
  122. package/pp-core/workflows/add-todo.md +1 -1
  123. package/pp-core/workflows/ai-integration-phase.md +2 -2
  124. package/pp-core/workflows/audit-fix.md +12 -16
  125. package/pp-core/workflows/audit-milestone.md +1 -1
  126. package/pp-core/workflows/audit-uat.md +1 -1
  127. package/pp-core/workflows/autonomous.md +68 -306
  128. package/pp-core/workflows/check-todos.md +1 -1
  129. package/pp-core/workflows/cleanup.md +1 -1
  130. package/pp-core/workflows/code-review-fix.md +1 -1
  131. package/pp-core/workflows/code-review.md +19 -7
  132. package/pp-core/workflows/complete-milestone.md +41 -60
  133. package/pp-core/workflows/debug.md +1 -1
  134. package/pp-core/workflows/diagnose-issues.md +1 -1
  135. package/pp-core/workflows/discovery-sprint/gap-analysis.md +140 -0
  136. package/pp-core/workflows/discovery-sprint/input-package.md +140 -0
  137. package/pp-core/workflows/discovery-sprint/prefill.md +92 -0
  138. package/pp-core/workflows/discovery-sprint/prototype.md +257 -0
  139. package/pp-core/workflows/discovery-sprint/rebaseline.md +276 -0
  140. package/pp-core/workflows/discovery-sprint/session-13-gates.md +24 -0
  141. package/pp-core/workflows/discovery-sprint/sessions/01-kick-off.md +92 -0
  142. package/pp-core/workflows/discovery-sprint/sessions/02-as-is.md +98 -0
  143. package/pp-core/workflows/discovery-sprint/sessions/03-to-be.md +215 -0
  144. package/pp-core/workflows/discovery-sprint/sessions/04-ux-design.md +146 -0
  145. package/pp-core/workflows/discovery-sprint/sessions/05-architecture.md +98 -0
  146. package/pp-core/workflows/discovery-sprint/sessions/06-security.md +107 -0
  147. package/pp-core/workflows/discovery-sprint/sessions/07-identity.md +98 -0
  148. package/pp-core/workflows/discovery-sprint/sessions/08-integration.md +85 -0
  149. package/pp-core/workflows/discovery-sprint/sessions/09-qa.md +109 -0
  150. package/pp-core/workflows/discovery-sprint/sessions/10-infrastructure.md +119 -0
  151. package/pp-core/workflows/discovery-sprint/sessions/11-roadmap.md +92 -0
  152. package/pp-core/workflows/discovery-sprint/sessions/12-delivery-model.md +98 -0
  153. package/pp-core/workflows/discovery-sprint/sessions/13-wrap-up.md +205 -0
  154. package/pp-core/workflows/discovery-sprint/templates/context-template.md +266 -0
  155. package/pp-core/workflows/discovery-sprint.md +79 -1714
  156. package/pp-core/workflows/discovery-to-build/steps/architecture-deliverables.md +5 -5
  157. package/pp-core/workflows/discovery-to-build/steps/screen-inventory-gate.md +70 -0
  158. package/pp-core/workflows/discovery-to-build/templates/discovery-references.md +54 -0
  159. package/pp-core/workflows/discovery-to-build/templates/pp-handoff.md +184 -0
  160. package/pp-core/workflows/discovery-to-build.md +21 -238
  161. package/pp-core/workflows/discuss-phase/modes/advisor.md +1 -1
  162. package/pp-core/workflows/discuss-phase/modes/auto.md +1 -1
  163. package/pp-core/workflows/discuss-phase/modes/chain.md +1 -1
  164. package/pp-core/workflows/discuss-phase-assumptions.md +1 -1
  165. package/pp-core/workflows/discuss-phase.md +9 -2
  166. package/pp-core/workflows/do.md +4 -4
  167. package/pp-core/workflows/docs-update.md +1 -1
  168. package/pp-core/workflows/edit-phase.md +1 -1
  169. package/pp-core/workflows/eval-review.md +1 -1
  170. package/pp-core/workflows/execute-phase/steps/checkpoint-handling.md +45 -0
  171. package/pp-core/workflows/execute-phase/steps/client-context-gate.md +1 -1
  172. package/pp-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
  173. package/pp-core/workflows/execute-phase/steps/cross-ai-delegation.md +90 -0
  174. package/pp-core/workflows/execute-phase/steps/install-run.md +29 -0
  175. package/pp-core/workflows/execute-phase/steps/phase-close-rebaseline.md +23 -0
  176. package/pp-core/workflows/execute-phase/steps/post-merge-gate.md +76 -77
  177. package/pp-core/workflows/execute-phase/steps/serial-group-dispatch.md +72 -0
  178. package/pp-core/workflows/execute-phase/steps/vault-fetch.md +5 -5
  179. package/pp-core/workflows/execute-phase/steps/vault-load-gate.md +19 -112
  180. package/pp-core/workflows/execute-phase.md +102 -216
  181. package/pp-core/workflows/execute-plan.md +6 -2
  182. package/pp-core/workflows/explore.md +1 -1
  183. package/pp-core/workflows/extract-learnings.md +1 -1
  184. package/pp-core/workflows/forensics.md +4 -4
  185. package/pp-core/workflows/graduation.md +1 -1
  186. package/pp-core/workflows/health.md +1 -1
  187. package/pp-core/workflows/help/modes/brief.md +1 -0
  188. package/pp-core/workflows/help/modes/default.md +2 -1
  189. package/pp-core/workflows/help/modes/full.md +12 -5
  190. package/pp-core/workflows/import.md +1 -1
  191. package/pp-core/workflows/ingest-docs.md +1 -1
  192. package/pp-core/workflows/init.md +18 -2
  193. package/pp-core/workflows/insert-phase.md +1 -1
  194. package/pp-core/workflows/list-workspaces.md +1 -1
  195. package/pp-core/workflows/manager.md +3 -3
  196. package/pp-core/workflows/map-codebase.md +2 -2
  197. package/pp-core/workflows/milestone-summary.md +4 -4
  198. package/pp-core/workflows/mvp-phase.md +1 -1
  199. package/pp-core/workflows/new-milestone.md +7 -8
  200. package/pp-core/workflows/new-project.md +6 -1
  201. package/pp-core/workflows/new-workspace.md +1 -1
  202. package/pp-core/workflows/next.md +2 -2
  203. package/pp-core/workflows/note.md +2 -2
  204. package/pp-core/workflows/pause-work.md +1 -1
  205. package/pp-core/workflows/plan-milestone-gaps.md +1 -1
  206. package/pp-core/workflows/plan-phase/steps/chunked-planning.md +111 -0
  207. package/pp-core/workflows/plan-phase/steps/phase-tier.md +34 -0
  208. package/pp-core/workflows/plan-phase.md +43 -167
  209. package/pp-core/workflows/plan-review-convergence.md +2 -2
  210. package/pp-core/workflows/plant-seed.md +1 -1
  211. package/pp-core/workflows/pr-branch.md +1 -1
  212. package/pp-core/workflows/profile-user.md +1 -1
  213. package/pp-core/workflows/progress.md +3 -3
  214. package/pp-core/workflows/quick.md +3 -1
  215. package/pp-core/workflows/remove-phase.md +1 -1
  216. package/pp-core/workflows/remove-workspace.md +1 -1
  217. package/pp-core/workflows/resume-project.md +6 -12
  218. package/pp-core/workflows/review.md +1 -1
  219. package/pp-core/workflows/scan.md +1 -1
  220. package/pp-core/workflows/secure-phase.md +1 -1
  221. package/pp-core/workflows/settings-advanced.md +1 -1
  222. package/pp-core/workflows/settings-integrations.md +1 -1
  223. package/pp-core/workflows/settings.md +1 -1
  224. package/pp-core/workflows/ship.md +25 -8
  225. package/pp-core/workflows/sketch-wrap-up.md +1 -1
  226. package/pp-core/workflows/sketch.md +2 -1
  227. package/pp-core/workflows/spec-phase.md +1 -1
  228. package/pp-core/workflows/spike-wrap-up.md +1 -1
  229. package/pp-core/workflows/spike.md +1 -1
  230. package/pp-core/workflows/stats.md +2 -2
  231. package/pp-core/workflows/thread.md +1 -1
  232. package/pp-core/workflows/transition.md +5 -9
  233. package/pp-core/workflows/ui-phase.md +23 -2
  234. package/pp-core/workflows/ui-review.md +1 -1
  235. package/pp-core/workflows/ultraplan-phase.md +2 -2
  236. package/pp-core/workflows/validate-phase.md +1 -1
  237. package/pp-core/workflows/vault-write-back.md +1 -1
  238. package/pp-core/workflows/verify-phase.md +29 -26
  239. package/pp-core/workflows/verify-work.md +14 -10
  240. package/scripts/base64-scan.sh +7 -41
  241. package/scripts/changeset/cli.cjs +25 -5
  242. package/scripts/dev-env.cjs +462 -0
  243. package/scripts/lib/allowlist-ratchet.cjs +103 -1
  244. package/scripts/lib/size-budget.cjs +311 -0
  245. package/scripts/lint-neutrality-names.cjs +694 -0
  246. package/scripts/lint-org-coordinates.allowlist.json +35 -0
  247. package/scripts/lint-org-coordinates.cjs +426 -0
  248. package/scripts/lint-skill-deps.cjs +48 -9
  249. package/scripts/lint-test-file-count.cjs +17 -2
  250. package/scripts/prompt-injection-scan.sh +41 -20
  251. package/scripts/sync-manifest-versions.cjs +41 -7
  252. package/scripts/sync-runtime-launcher.cjs +106 -1
  253. package/scripts/update-size-baseline.cjs +274 -30
  254. package/scripts/workflow-size.cjs +61 -9
  255. package/commands/pp-build-handoff.md +0 -46
  256. 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
+ };