@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,35 @@
1
+ {
2
+ "_doc": "Placeholder Azure DevOps orgs and org-profile package names exempted from the org-coordinate guard (scripts/lint-org-coordinates.cjs), each with a reason (D-09). `exempt_paths` holds the guard's single exemption and must contain exactly one entry (D-11). Pareto's own identities are listed individually, never by scope wildcard (D-07). Nothing in this file may name a real client or partner, and the CI name scan (TEN-09) enforces that independently.",
3
+ "exempt_paths": [".planning/milestones/"],
4
+ "orgs": [
5
+ { "value": "contoso", "reason": "Phase 37 canonical placeholder org; covers its Platform project, the Platform-Vault and shared-vault repos, the other-project fixture, and the HTTPS and SSH forms" },
6
+ { "value": "contoso-org", "reason": "Phase 37 canonical second placeholder org" },
7
+ { "value": "o", "reason": "pre-existing minimal placeholder" },
8
+ { "value": "org", "reason": "pre-existing generic placeholder, including the org/project/_git/repo worked example in docs, schemas, src and CHANGELOG" },
9
+ { "value": "my-org", "reason": "pre-existing placeholder" },
10
+ { "value": "x", "reason": "pre-existing minimal placeholder" },
11
+ { "value": "REPLACE-org", "reason": "template placeholder in packages/org-contoso.TEMPLATE/org-profile.json" },
12
+ { "value": "shared", "reason": "test-fixture fallback coordinate, a generic word and not an organisation" },
13
+ { "value": "pareto4", "reason": "Pareto's own organisation (D-07)" }
14
+ ],
15
+ "packages": [
16
+ { "value": "@pareto4/org-contoso", "reason": "canonical placeholder package" },
17
+ { "value": "@pareto4/org-pareto", "reason": "Pareto's own org profile (D-07)" },
18
+ { "value": "@pareto4/org-example", "reason": "neutral worked example" },
19
+ { "value": "@your-scope/org-your-slug", "reason": "docs worked example" },
20
+ { "value": "@your-org/org-profile-package", "reason": "docs worked example" },
21
+ { "value": "@pp-test/org-fixture", "reason": "test fixture scope" },
22
+ { "value": "@fixture-scope/org-fixture", "reason": "test fixture scope" },
23
+ { "value": "@ppfixture/org-test", "reason": "test fixture scope" },
24
+ { "value": "@packages/org-pareto", "reason": "a path into Pareto's own packages/org-pareto directory written with a leading @ (PLAN transclusion or a quoted mention), not an npm scope (D-07)" }
25
+ ],
26
+ "seed_notes": [
27
+ { "value": "contoso-shared-vault", "reason": "repository name, covered by org contoso" },
28
+ { "value": "org-contoso.TEMPLATE", "reason": "directory name, no scope" },
29
+ { "value": "<org>/<project>", "reason": "angle brackets never match" },
30
+ { "value": "acme-vault", "reason": "GitLab path, outside D-08" },
31
+ { "value": "pareto4-clients/acme/acme-vault", "reason": "GitLab path, outside D-08" },
32
+ { "value": "vault.example.com", "reason": "hostname, not an Azure shape" },
33
+ { "value": "client-host.example.com", "reason": "hostname, not an Azure shape" }
34
+ ]
35
+ }
@@ -0,0 +1,426 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * lint-org-coordinates.cjs — TEN-08 build-time organisation-coordinate guard.
6
+ *
7
+ * Design (D-07..D-11, .planning/phases/38-tenant-neutrality-planning-records-guards/38-CONTEXT.md):
8
+ * D-08 "Real-looking organisation coordinate" = Azure DevOps shapes. The
9
+ * organisation segment alone suffices after the Azure DevOps host —
10
+ * under any subdomain (e.g. pkgs.dev.azure.com, feeds.dev.azure.com),
11
+ * with any following path including `_`-prefixed service paths
12
+ * (/_packaging, /_apis, a trailing slash, or nothing at all) — and in
13
+ * the SSH form (ssh.dev.azure.com:v3/<org>). <org>.visualstudio.com
14
+ * and <org>/<project>/_git/<repo> are also covered, as is the
15
+ * @<scope>/org-<slug> org-profile package name shape. Host matching
16
+ * is case-insensitive (dev.azure.com, Dev.Azure.com, VisualStudio.com
17
+ * all match) and so is the `_git` segment. Every line is matched
18
+ * through a normalised copy (WR-11, IN-13, IN-14). The normaliser
19
+ * decodes exactly these spellings, case-insensitively, and nothing
20
+ * else:
21
+ * - up to three rounds of `%25` to `%` (never a loop until stable);
22
+ * - `%2F` to `/` and `%3A` to `:`;
23
+ * - the JSON escape (a backslash followed by `u002F`) and the hex
24
+ * escape (a backslash followed by `x2F`), each to `/`;
25
+ * - the decimal entity `&#47;` and the hex entity `&#x2F;`, each
26
+ * with any number of leading zeros and with or without the
27
+ * semicolon, except that a semicolon-less entity followed by
28
+ * another digit of its base is left alone (a numeric reference
29
+ * consumes every following digit, so it is a different character);
30
+ * - the named entity `&sol;`;
31
+ * - any run of backslashes directly before `/`, to `/`, in one
32
+ * linear pass (IN-13).
33
+ * On that copy azure_host accepts a
34
+ * trailing-dot host, an explicit port and one or more slashes after
35
+ * the host, and azure_ssh accepts a trailing-dot host and repeated
36
+ * slashes. The allowlist comparison itself stays exact and
37
+ * case-sensitive — a capitalised variant of an allowlisted org is still
38
+ * a violation. A tracked line containing one of these shapes whose
39
+ * org/package is not on the committed allowlist makes this guard exit
40
+ * 1. Columns refer to the raw line: the raw column of the match when
41
+ * normalisation changed nothing before it, column 1 when it did.
42
+ * D-09 The allowlist (scripts/lint-org-coordinates.allowlist.json) is a
43
+ * committed JSON file of typed, exact-match placeholder values, each
44
+ * with a reason — never a wildcard, never a client name.
45
+ * D-07 Pareto's own identities are allowlisted individually; no
46
+ * @pareto4/* scope wildcard, so a future real org-profile package
47
+ * still requires an explicit, reviewed allowlist addition.
48
+ * D-11 Scans tracked files only (`git ls-files`), with exactly one
49
+ * explicitly-listed exemption path (.planning/milestones/) — a
50
+ * near-miss prefix such as .planning/milestones-archive/ is still
51
+ * scanned.
52
+ *
53
+ * Scope (IN-07): this guard scans only the tracked tree at HEAD. Git
54
+ * history, commit messages, branch and tag names, and MR titles are outside
55
+ * its reach — a name removed by a rewording or rename can still be present
56
+ * in history. If the repository is ever made public or mirrored, plan a
57
+ * separate history scan or rewrite.
58
+ *
59
+ * Residual (what the normalised copy above does not cover; deferred to
60
+ * .planning/todos/pending/neutrality-guard-review-deferred-findings.md): any
61
+ * spelling outside the decode list is not decoded — for example
62
+ * percent-encoded dots or `@`, numeric entities for characters other than `/`,
63
+ * more than four levels of percent-encoding, and look-alike Unicode — plus the
64
+ * deferred WR-07 and WR-08, and IN-12's quadratic worst case in the
65
+ * visualstudio and git_path patterns on long hyphen- or dot-separated lines.
66
+ * Lines are scanned in full by design: a line-length cap would let a
67
+ * coordinate at the end of a long line pass silently (fail open).
68
+ *
69
+ * A true positive is, by definition, a real organisation coordinate — so
70
+ * unlike scripts/verify-rename-guard.cjs's report line, this guard NEVER
71
+ * prints the matched text. It reports file:line:col and the shape only,
72
+ * keeping CI logs from spelling an organisation even when they catch one.
73
+ * Every example below uses `<org>`, never an alphanumeric org segment.
74
+ *
75
+ * Env overrides (test-only, mirrors ORG_COORD_GUARD_* in
76
+ * tests/lint-org-coordinates.test.cjs):
77
+ * ORG_COORD_GUARD_REPO_ROOT — redirect the whole-tree scan to a temp git repo
78
+ * ORG_COORD_GUARD_ALLOWLIST_PATH — redirect the allowlist file to a temp path
79
+ *
80
+ * Exit codes:
81
+ * 0 — no violation found
82
+ * 1 — one or more violations found, or the allowlist / git ls-files failed
83
+ *
84
+ * Usage:
85
+ * node scripts/lint-org-coordinates.cjs
86
+ *
87
+ * Exports:
88
+ * PATTERNS, normaliseForMatch(line), loadAllowlist(path), validateAllowlist(obj),
89
+ * runScan({ repoRoot, allowlistPath } = {}), cliMain
90
+ */
91
+
92
+ const { execFileSync } = require('node:child_process');
93
+ const fs = require('node:fs');
94
+ const path = require('node:path');
95
+
96
+ const { ExitError, runMain } = require('./lib/cli-exit.cjs');
97
+
98
+ // SEG matches an Azure DevOps org/project/repo path segment.
99
+ const SEG = '[A-Za-z0-9][A-Za-z0-9_.-]*';
100
+
101
+ // HOST_TAIL follows an Azure DevOps host on the normalised line: an optional
102
+ // trailing dot, an optional explicit port, and one or more slashes.
103
+ const HOST_TAIL = '\\.?(?::\\d{1,5})?/+';
104
+
105
+ /**
106
+ * normaliseForMatch(line) — the copy of a line every pattern runs against
107
+ * (WR-11). Bounded, never a loop-until-stable: at most three rounds of
108
+ * percent-25 decoding.
109
+ *
110
+ * @param {string} line
111
+ * @returns {string}
112
+ */
113
+ function normaliseForMatch(line) {
114
+ let s = line;
115
+ for (let round = 0; round < 3 && /%25/i.test(s); round++) {
116
+ s = s.replace(/%25/gi, '%');
117
+ }
118
+ s = s.replace(/%2F/gi, '/').replace(/%3A/gi, ':');
119
+ s = s.replace(new RegExp('\\\\' + 'u002F', 'gi'), '/');
120
+ s = s.replace(new RegExp('\\\\' + 'x2F', 'gi'), '/');
121
+ // A numeric reference consumes every following digit of its base, so a
122
+ // semicolon-less form must not be followed by another digit of that base.
123
+ s = s.replace(/&#0*47(?:;|(?![0-9]))|&#x0*2F(?:;|(?![0-9a-f]))|&sol;/gi, '/');
124
+ // IN-13: one linear pass — each maximal backslash run is matched once; a run
125
+ // followed by `/` collapses to `/`, any other run is kept as it is.
126
+ s = s.replace(/\\+(\/)?/g, (run, slash) => (slash ? '/' : run));
127
+ return s;
128
+ }
129
+
130
+ // D-08 pattern set. Object key order is part of this module's public
131
+ // contract (tests assert Object.keys(PATTERNS) deep-equals this order).
132
+ // azure_host / azure_ssh capture the organisation segment alone (CR-02): any
133
+ // subdomain and any following path (including `_`-prefixed service paths)
134
+ // matches, because the pattern is not anchored past the org segment. Both
135
+ // carry the 'i' flag for case-insensitive host matching (WR-01); visualstudio
136
+ // gains 'i' for the same reason. The allowlist comparison itself stays exact
137
+ // and case-sensitive (see collectViolations below).
138
+ const PATTERNS = {
139
+ azure_host: new RegExp('dev\\.azure\\.com' + HOST_TAIL + '(' + SEG + ')', 'gi'),
140
+ azure_ssh: new RegExp('ssh\\.dev\\.azure\\.com\\.?:v3/+(' + SEG + ')', 'gi'),
141
+ visualstudio: /\b([A-Za-z0-9][A-Za-z0-9-]*)\.visualstudio\.com\b/gi,
142
+ git_path: new RegExp('\\b(' + SEG + ')/(' + SEG + ')/_git/(' + SEG + ')', 'gi'),
143
+ org_pkg: /@([a-z0-9][a-z0-9-]*)\/org-([a-z0-9][a-z0-9._-]*)/g,
144
+ };
145
+
146
+ const VS_SUFFIX = '.visualstudio.com';
147
+
148
+ // ORG_COORD_GUARD_REPO_ROOT lets tests redirect the guard to a temporary git
149
+ // repo without touching the real working tree.
150
+ const REPO_ROOT = process.env.ORG_COORD_GUARD_REPO_ROOT
151
+ ? path.resolve(process.env.ORG_COORD_GUARD_REPO_ROOT)
152
+ : path.resolve(__dirname, '..');
153
+
154
+ // Default allowlist path lives next to this script.
155
+ const DEFAULT_ALLOWLIST_PATH = path.join(__dirname, 'lint-org-coordinates.allowlist.json');
156
+ const ALLOWLIST_PATH = process.env.ORG_COORD_GUARD_ALLOWLIST_PATH
157
+ ? path.resolve(process.env.ORG_COORD_GUARD_ALLOWLIST_PATH)
158
+ : DEFAULT_ALLOWLIST_PATH;
159
+
160
+ /**
161
+ * isBinary(fullPath) — NUL-byte probe on the first 8 KiB.
162
+ *
163
+ * @param {string} fullPath absolute path to probe
164
+ * @returns {boolean}
165
+ */
166
+ function isBinary(fullPath) {
167
+ let fd;
168
+ try {
169
+ fd = fs.openSync(fullPath, 'r');
170
+ const buf = Buffer.allocUnsafe(8192);
171
+ const bytesRead = fs.readSync(fd, buf, 0, 8192, 0);
172
+ return buf.subarray(0, bytesRead).includes(0);
173
+ } catch {
174
+ return false;
175
+ } finally {
176
+ if (fd != null) {
177
+ try { fs.closeSync(fd); } catch { /* best-effort */ }
178
+ }
179
+ }
180
+ }
181
+
182
+ /**
183
+ * trimTrailing(str) — strips trailing `.`, `-`, `_` from a captured org or
184
+ * package slug before comparison, so a sentence-final period never causes a
185
+ * false violation.
186
+ *
187
+ * @param {string} str
188
+ * @returns {string}
189
+ */
190
+ function trimTrailing(str) {
191
+ return str.replace(/[._-]+$/, '');
192
+ }
193
+
194
+ /**
195
+ * loadAllowlist(allowlistPath) — read and JSON-parse the allowlist file. A
196
+ * read or parse failure is reported as an allowlist-invalid config error
197
+ * (exit 1), matching validateAllowlist()'s own failure shape.
198
+ *
199
+ * @param {string} allowlistPath
200
+ * @returns {object}
201
+ */
202
+ function loadAllowlist(allowlistPath) {
203
+ let raw;
204
+ try {
205
+ raw = fs.readFileSync(allowlistPath, 'utf8');
206
+ } catch (err) {
207
+ throw new ExitError(1, 'ERROR lint-org-coordinates: allowlist invalid — could not read allowlist file: ' + err.message);
208
+ }
209
+ try {
210
+ return JSON.parse(raw);
211
+ } catch (err) {
212
+ throw new ExitError(1, 'ERROR lint-org-coordinates: allowlist invalid — invalid JSON: ' + err.message);
213
+ }
214
+ }
215
+
216
+ /**
217
+ * validateAllowlist(obj) — structural validation of a loaded allowlist.
218
+ *
219
+ * @param {object} obj
220
+ * @returns {{ ok: boolean, reason: string|null }}
221
+ */
222
+ function validateAllowlist(obj) {
223
+ if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {
224
+ return { ok: false, reason: 'allowlist is not an object' };
225
+ }
226
+
227
+ if (!Array.isArray(obj.exempt_paths) || obj.exempt_paths.length !== 1) {
228
+ return { ok: false, reason: 'exempt_paths must be an array of exactly one string' };
229
+ }
230
+ const exemptPath = obj.exempt_paths[0];
231
+ if (typeof exemptPath !== 'string' || exemptPath === '' || !exemptPath.endsWith('/')) {
232
+ return { ok: false, reason: 'exempt_paths entry must be a non-empty string ending in "/"' };
233
+ }
234
+
235
+ for (const listName of ['orgs', 'packages', 'seed_notes']) {
236
+ const list = obj[listName];
237
+ if (!Array.isArray(list)) {
238
+ return { ok: false, reason: listName + ' must be an array' };
239
+ }
240
+ const seen = new Set();
241
+ for (const entry of list) {
242
+ if (
243
+ !entry ||
244
+ typeof entry !== 'object' ||
245
+ typeof entry.value !== 'string' ||
246
+ entry.value === '' ||
247
+ typeof entry.reason !== 'string' ||
248
+ entry.reason === ''
249
+ ) {
250
+ return { ok: false, reason: listName + ' has an entry missing a non-empty value or reason' };
251
+ }
252
+ if (entry.value.includes('*')) {
253
+ return { ok: false, reason: listName + ' entry "' + entry.value + '" contains a wildcard "*"' };
254
+ }
255
+ if (seen.has(entry.value)) {
256
+ return { ok: false, reason: listName + ' has a duplicate value "' + entry.value + '"' };
257
+ }
258
+ seen.add(entry.value);
259
+ }
260
+ }
261
+
262
+ const orgPkgShape = /^@[a-z0-9][a-z0-9-]*\/org-[a-z0-9][a-z0-9._-]*$/;
263
+ for (const entry of obj.packages) {
264
+ if (!orgPkgShape.test(entry.value)) {
265
+ return { ok: false, reason: 'packages entry "' + entry.value + '" does not match the org_pkg shape' };
266
+ }
267
+ }
268
+
269
+ return { ok: true, reason: null };
270
+ }
271
+
272
+ /**
273
+ * collectViolations(repoRoot, allowlist) — enumerate the whole tracked tree
274
+ * (minus the single exempt path prefix) and collect every PATTERNS hit whose
275
+ * resolved org/package is not on the allowlist.
276
+ *
277
+ * @param {string} repoRoot
278
+ * @param {object} allowlist a validated allowlist object
279
+ * @returns {{ violations: Array<{file: string, line: number, col: number, shape: string}>, fileCount: number }}
280
+ */
281
+ function collectViolations(repoRoot, allowlist) {
282
+ let trackedOutput;
283
+ try {
284
+ trackedOutput = execFileSync('git', ['ls-files', '-z'], { cwd: repoRoot, encoding: 'utf8' });
285
+ } catch (err) {
286
+ throw new ExitError(1, 'ERROR lint-org-coordinates: git ls-files failed: ' + err.message);
287
+ }
288
+ const trackedFiles = trackedOutput.split('\u0000').filter(Boolean);
289
+
290
+ const exemptPaths = allowlist.exempt_paths;
291
+ const orgSet = new Set(allowlist.orgs.map((e) => e.value));
292
+ const packageSet = new Set(allowlist.packages.map((e) => e.value));
293
+
294
+ /** @type {Array<{file: string, line: number, col: number, shape: string}>} */
295
+ const violations = [];
296
+ const seen = new Set();
297
+
298
+ for (const relPath of trackedFiles) {
299
+ if (exemptPaths.some((ep) => relPath.startsWith(ep))) continue;
300
+
301
+ const fullPath = path.join(repoRoot, relPath);
302
+ if (isBinary(fullPath)) continue;
303
+
304
+ let content;
305
+ try {
306
+ content = fs.readFileSync(fullPath, 'utf8');
307
+ } catch {
308
+ continue;
309
+ }
310
+
311
+ const lines = content.split('\n');
312
+ for (let i = 0; i < lines.length; i++) {
313
+ const rawLine = lines[i];
314
+ const matchLine = normaliseForMatch(rawLine);
315
+ for (const shape of Object.keys(PATTERNS)) {
316
+ const re = PATTERNS[shape];
317
+ re.lastIndex = 0;
318
+ let m = re.exec(matchLine);
319
+ while (m !== null) {
320
+ // Column refers to the raw line: exact when normalisation changed
321
+ // nothing before the match, otherwise column 1.
322
+ const col = matchLine.slice(0, m.index) === rawLine.slice(0, m.index) ? m.index + 1 : 1;
323
+ let allowed;
324
+
325
+ if (shape === 'org_pkg') {
326
+ const scope = m[1];
327
+ const slug = trimTrailing(m[2]);
328
+ const pkg = '@' + scope + '/org-' + slug;
329
+ allowed = packageSet.has(pkg);
330
+ } else {
331
+ let org = m[1];
332
+ if (shape === 'git_path' && org.toLowerCase().endsWith(VS_SUFFIX)) {
333
+ org = org.slice(0, -VS_SUFFIX.length);
334
+ }
335
+ org = trimTrailing(org);
336
+ allowed = orgSet.has(org);
337
+ }
338
+
339
+ if (!allowed) {
340
+ const key = relPath + ':' + (i + 1) + ':' + m.index + ':' + shape;
341
+ if (!seen.has(key)) {
342
+ seen.add(key);
343
+ violations.push({ file: relPath, line: i + 1, col, shape });
344
+ }
345
+ }
346
+
347
+ // Guard against a zero-length match causing an infinite loop.
348
+ if (m[0].length === 0) re.lastIndex += 1;
349
+ m = re.exec(matchLine);
350
+ }
351
+ }
352
+ }
353
+ }
354
+
355
+ return { violations, fileCount: trackedFiles.length };
356
+ }
357
+
358
+ /**
359
+ * runScan(opts?) — pure scan entry point (no process.exit, no stdout/stderr
360
+ * writes). Consumed by the CLI wrapper (cliMain) and directly by tests.
361
+ *
362
+ * @param {object} [opts]
363
+ * @param {string} [opts.repoRoot] defaults to the module-level REPO_ROOT
364
+ * @param {string} [opts.allowlistPath] defaults to the module-level ALLOWLIST_PATH
365
+ * @returns {{
366
+ * violations: Array<{file: string, line: number, col: number, shape: string}>,
367
+ * fileCount: number,
368
+ * exemptPaths: string[],
369
+ * repoRoot: string,
370
+ * allowlistPath: string,
371
+ * }}
372
+ */
373
+ function runScan(opts) {
374
+ const repoRoot = (opts && opts.repoRoot) || REPO_ROOT;
375
+ const allowlistPath = (opts && opts.allowlistPath) || ALLOWLIST_PATH;
376
+
377
+ const allowlist = loadAllowlist(allowlistPath);
378
+ const validation = validateAllowlist(allowlist);
379
+ if (!validation.ok) {
380
+ throw new ExitError(1, 'ERROR lint-org-coordinates: allowlist invalid — ' + validation.reason);
381
+ }
382
+
383
+ const { violations, fileCount } = collectViolations(repoRoot, allowlist);
384
+
385
+ return {
386
+ violations,
387
+ fileCount,
388
+ exemptPaths: allowlist.exempt_paths,
389
+ repoRoot,
390
+ allowlistPath,
391
+ };
392
+ }
393
+
394
+ // ── CLI entry ────────────────────────────────────────────────────────────────
395
+
396
+ function cliMain() {
397
+ const { violations, fileCount, exemptPaths } = runScan();
398
+
399
+ if (violations.length === 0) {
400
+ process.stdout.write(
401
+ 'ok lint-org-coordinates: ' + fileCount + ' tracked files scanned, 0 violations (single exemption: ' +
402
+ exemptPaths[0] + ')\n'
403
+ );
404
+ return 0;
405
+ }
406
+
407
+ process.stderr.write(
408
+ 'FAIL lint-org-coordinates: ' + violations.length +
409
+ ' organisation coordinate(s) outside the placeholder allowlist\n'
410
+ );
411
+ for (const v of violations) {
412
+ process.stderr.write(' ' + v.file + ':' + v.line + ':' + v.col + ' — shape=' + v.shape + '\n');
413
+ }
414
+ process.stderr.write(
415
+ 'use a placeholder from scripts/lint-org-coordinates.allowlist.json (e.g. org contoso, package ' +
416
+ '@pareto4/org-contoso) or add a placeholder entry with a reason\n'
417
+ );
418
+
419
+ throw new ExitError(1);
420
+ }
421
+
422
+ module.exports = { PATTERNS, normaliseForMatch, loadAllowlist, validateAllowlist, runScan, cliMain };
423
+
424
+ if (require.main === module) {
425
+ runMain(cliMain);
426
+ }
@@ -5,7 +5,7 @@
5
5
  * Two checks:
6
6
  *
7
7
  * a) Frontmatter to body consistency:
8
- * For each commands/gsd/*.md, parse requires: and walk the body for
8
+ * For each commands/pp-*.md, parse requires: and walk the body for
9
9
  * references to other GSD skills (pattern: /gsd:<stem> or gsd:<stem>).
10
10
  * Fail if a skill body references a skill not listed in requires:.
11
11
  *
@@ -15,10 +15,14 @@
15
15
  * the closure references a skill NOT in the closure, fail.
16
16
  *
17
17
  * Usage:
18
- * node scripts/lint-skill-deps.cjs # scans commands/gsd/
18
+ * node scripts/lint-skill-deps.cjs # scans commands/
19
19
  * node scripts/lint-skill-deps.cjs --dir <path> # scan a custom dir (testing)
20
20
  *
21
- * Exits 0 if all pass; exits 1 if any violation.
21
+ * Exits 0 if all pass; exits 1 if any violation, and exits 1 when the scanned
22
+ * directory is missing or holds no skills — a run that checked nothing must not
23
+ * report ok. (The default used to be a legacy nested commands subdirectory, which
24
+ * stopped existing at the D-12 flat-hyphen move; for that whole period lint:ci printed
25
+ * "0 skill(s) checked, 0 violations" and passed.)
22
26
  */
23
27
 
24
28
  'use strict';
@@ -34,7 +38,8 @@ const { PROFILES, loadSkillsManifest, resolveProfile } = require(PROFILES_MODULE
34
38
  // Argument parsing
35
39
  // ---------------------------------------------------------------------------
36
40
 
37
- let commandsDir = path.join(__dirname, '..', 'commands', 'gsd');
41
+ // D-12 flat-hyphen: commands ship flat as commands/pp-<stem>.md (no nested subdir).
42
+ let commandsDir = path.join(__dirname, '..', 'commands');
38
43
  const args = process.argv.slice(2);
39
44
  for (let i = 0; i < args.length; i++) {
40
45
  if (args[i] === '--dir' && args[i + 1]) {
@@ -67,20 +72,36 @@ function extractBody(content) {
67
72
  // Check a: frontmatter to body consistency
68
73
  // ---------------------------------------------------------------------------
69
74
 
75
+ // The manifest keys are bare stems ('pp-progress.md' -> 'progress'), so the
76
+ // file has to be found again: shipped commands are pp-<stem>.md, while bare
77
+ // <stem>.md is kept for fixtures. Rebuilding the path as <stem>.md alone
78
+ // matched no shipped file, and every read fell into the catch below.
79
+ function resolveSkillFile(stem) {
80
+ for (const name of ['pp-' + stem + '.md', stem + '.md']) {
81
+ const candidate = path.join(commandsDir, name);
82
+ if (fs.existsSync(candidate)) return candidate;
83
+ }
84
+ return null;
85
+ }
86
+
70
87
  function checkFrontmatterBodyConsistency(manifest, allStems) {
71
88
  const violations = [];
89
+ let filesRead = 0;
90
+ let refsFound = 0;
72
91
 
73
92
  for (const [stem, declared] of manifest) {
74
93
  if (stem.startsWith('_calls_agents_')) continue;
75
- const filePath = path.join(commandsDir, stem + '.md');
94
+ const filePath = resolveSkillFile(stem);
76
95
  let content;
77
96
  try {
78
97
  content = fs.readFileSync(filePath, 'utf8');
79
98
  } catch {
80
99
  continue;
81
100
  }
101
+ filesRead++;
82
102
  const body = extractBody(content);
83
103
  const referenced = extractBodyReferences(body);
104
+ refsFound += [...referenced].filter((ref) => ref !== stem).length;
84
105
  const declaredSet = new Set(declared);
85
106
  for (const ref of referenced) {
86
107
  if (ref === stem) continue;
@@ -104,7 +125,7 @@ function checkFrontmatterBodyConsistency(manifest, allStems) {
104
125
  }
105
126
  }
106
127
 
107
- return violations;
128
+ return { violations, filesRead, refsFound };
108
129
  }
109
130
 
110
131
  // ---------------------------------------------------------------------------
@@ -149,14 +170,32 @@ function main() {
149
170
  [...manifest.keys()].filter((k) => !k.startsWith('_calls_agents_'))
150
171
  );
151
172
 
152
- const consistencyViolations = checkFrontmatterBodyConsistency(manifest, allStems);
173
+ if (allStems.size === 0) {
174
+ process.stderr.write(
175
+ '\nERROR lint-skill-deps: no skills found in ' + commandsDir + '\n' +
176
+ 'A run that checks zero skills proves nothing. Point --dir at a directory\n' +
177
+ 'holding pp-<stem>.md command files (the default is commands/).\n\n'
178
+ );
179
+ return 1;
180
+ }
181
+
182
+ const consistency = checkFrontmatterBodyConsistency(manifest, allStems);
183
+ const consistencyViolations = consistency.violations;
153
184
  const closureViolations = checkProfileClosure(manifest);
154
185
 
155
186
  const totalViolations = consistencyViolations.length + closureViolations.length;
156
187
 
157
188
  if (totalViolations === 0) {
158
- const checked = manifest.size;
159
- process.stdout.write('ok lint-skill-deps: ' + checked + ' skill(s) checked, 0 violations\n');
189
+ // Count skills, not manifest entries: the manifest also holds one
190
+ // _calls_agents_<stem> key per skill, which doubled the old figure.
191
+ // The cross-reference count is printed so a pass with 0 references reads as
192
+ // what it is: check (a) scans for the gsd:<stem> form, which shipped bodies
193
+ // no longer use (todo: lint-skill-deps-check-a-scans-a-token-no-command-uses).
194
+ process.stdout.write(
195
+ 'ok lint-skill-deps: ' + allStems.size + ' skill(s) checked (' +
196
+ consistency.filesRead + ' body file(s) read, ' + consistency.refsFound +
197
+ ' cross-reference(s) found), 0 violations\n'
198
+ );
160
199
  return 0;
161
200
  }
162
201
 
@@ -11,6 +11,12 @@
11
11
  *
12
12
  * Verdicts: OK_UNDER_LIMIT | OK_IN_ALLOWLIST | FAIL_EXCEEDS_LIMIT |
13
13
  * FAIL_NOVEL_FILES | FAIL_STALE_ALLOWLIST
14
+ *
15
+ * BUILD BEFORE LINT. Attribution reads the COMPILED production modules under
16
+ * pp-core/bin/lib/, so `npm run build:lib` must precede this gate: on an
17
+ * unbuilt tree the module a test belongs to does not exist yet, and the test
18
+ * is attributed to whichever coarser prefix happens to match instead — the
19
+ * gate then reports novel files that are only novel because nothing compiled.
14
20
  */
15
21
  'use strict';
16
22
 
@@ -70,10 +76,19 @@ function prodPrefix(filename) {
70
76
  return filename.replace(/\.(cjs|ts|js)$/, '');
71
77
  }
72
78
 
73
- // Strip .test.{cjs,ts} and .integration.test.ts, then strip issue stamps.
79
+ // Strip the convention suffixes, then the bare .test.{cjs,ts}, then issue
80
+ // stamps. Five forms are stripped: `.integration`, `.slow`, `.security`,
81
+ // `.install` and the bare `.test`.
82
+ //
83
+ // `.property` is DELIBERATELY NOT stripped. The `vault` entry in
84
+ // scripts/lint-test-file-count.allowlist.json documents
85
+ // vault-practices.property.test.cjs as parked in that bucket precisely because
86
+ // this function leaves the suffix on — stripping it would re-cluster the file
87
+ // into `vault-practices` and turn that entry's own explanation into a false
88
+ // statement. Changing it is a deliberate allowlist migration, not a tidy-up.
74
89
  function testEffectivePrefix(testName) {
75
90
  const bare = testName
76
- .replace(/\.integration\.test\.(ts|cjs)$/, '')
91
+ .replace(/\.(integration|slow|security|install)\.test\.(ts|cjs)$/, '')
77
92
  .replace(/\.test\.(ts|cjs)$/, '');
78
93
  const m = bare.match(/^(?:feat|bug|enh|fix)-\d+(?:-\d+)*-(.+)$/);
79
94
  return m ? m[1] : bare;