@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,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 `/` and the hex entity `/`, 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 `/`;
|
|
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(/�*47(?:;|(?![0-9]))|�*2F(?:;|(?![0-9a-f]))|//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/
|
|
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/
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
159
|
-
|
|
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}
|
|
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;
|