@pareto4/pp-core 1.13.0 → 1.18.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 +3 -3
- package/README.md +31 -12
- package/agents/pp-code-fixer.md +0 -11
- package/agents/pp-code-reviewer.md +1 -77
- package/agents/pp-debug-session-manager.md +9 -6
- package/agents/pp-executor.md +12 -29
- package/agents/pp-plan-checker.md +4 -4
- package/agents/pp-planner.md +34 -20
- package/agents/pp-vault-reviewer.md +100 -23
- package/agents/pp-verifier.md +1 -3
- package/bin/install.js +10 -3
- package/commands/pp-execute-phase.md +1 -3
- package/commands/pp-ns-manage.md +1 -1
- package/commands/pp-ns-review.md +1 -1
- package/commands/pp-plan-phase.md +1 -2
- package/commands/pp-vault-promote.md +7 -8
- package/commands/pp-vault-review.md +6 -5
- package/commands/pp-verify-work.md +1 -2
- package/gemini-extension.json +1 -1
- package/hooks/dist/pp-check-update.js +0 -0
- package/hooks/dist/pp-cursor-session-start.js +1 -1
- package/hooks/dist/pp-statusline.js +1 -1
- package/hooks/dist/pp-update-banner.js +0 -0
- package/hooks/pp-check-update.js +0 -0
- package/hooks/pp-cursor-session-start.js +1 -1
- package/hooks/pp-statusline.js +1 -1
- package/hooks/pp-update-banner.js +0 -0
- package/package.json +10 -18
- package/pareto-skills/_shared/create-project-board.sh +0 -0
- package/pareto-skills/pp-project-setup/SKILL.md +1 -1
- package/pp-core/bin/check-latest-version.cjs +0 -0
- package/pp-core/bin/detect-build-commands.sh +2 -2
- package/pp-core/bin/lib/capability-activation.cjs +83 -9
- package/pp-core/bin/lib/capability-registry.cjs +201 -229
- package/pp-core/bin/lib/capability-state.cjs +93 -1
- package/pp-core/bin/lib/capability-writer.cjs +3 -3
- package/pp-core/bin/lib/check-command-router.cjs +1906 -1514
- package/pp-core/bin/lib/clusters.cjs +3 -0
- package/pp-core/bin/lib/command-aliases.cjs +0 -35
- package/pp-core/bin/lib/commands.cjs +9 -4
- package/pp-core/bin/lib/compose-bindings.cjs +251 -88
- package/pp-core/bin/lib/config-loader.cjs +7 -103
- package/pp-core/bin/lib/config-schema.cjs +1 -1
- package/pp-core/bin/lib/config.cjs +151 -62
- package/pp-core/bin/lib/decisions.cjs +4 -4
- package/pp-core/bin/lib/discovery-vault-check.cjs +246 -0
- package/pp-core/bin/lib/frontmatter.cjs +1 -38
- package/pp-core/bin/lib/git-base-branch.cjs +11 -12
- package/pp-core/bin/lib/git-provider.cjs +9 -10
- package/pp-core/bin/lib/init-command-router.cjs +5 -8
- package/pp-core/bin/lib/init-contract.cjs +620 -738
- package/pp-core/bin/lib/init-interactive.cjs +62 -4
- package/pp-core/bin/lib/init.cjs +24 -51
- package/pp-core/bin/lib/install-profiles.cjs +1 -1
- package/pp-core/bin/lib/io.cjs +4 -2
- package/pp-core/bin/lib/loop-resolver.cjs +165 -129
- package/pp-core/bin/lib/org-profile.cjs +402 -161
- package/pp-core/bin/lib/package-identity.cjs +3 -3
- package/pp-core/bin/lib/phase-command-router.cjs +1 -29
- package/pp-core/bin/lib/phase-scope.cjs +1 -1
- package/pp-core/bin/lib/phase.cjs +11 -253
- package/pp-core/bin/lib/platform-bindings.cjs +40 -19
- package/pp-core/bin/lib/profile-output.cjs +1 -1
- package/pp-core/bin/lib/shell-command-projection.cjs +104 -1
- package/pp-core/bin/lib/state.cjs +11 -85
- package/pp-core/bin/lib/template.cjs +1 -1
- package/pp-core/bin/lib/vault-command-router.cjs +1682 -477
- package/pp-core/bin/lib/vault-coordinates.cjs +16 -7
- package/pp-core/bin/lib/vault-index.cjs +656 -0
- package/pp-core/bin/lib/vault-seal.cjs +1851 -0
- package/pp-core/bin/lib/vault-standards-promote.cjs +577 -0
- package/pp-core/bin/lib/vault-standards-review.cjs +293 -0
- package/pp-core/bin/lib/vault-standards.cjs +1504 -0
- package/pp-core/bin/lib/vault-trace.cjs +155 -2778
- package/pp-core/bin/lib/vault-update.cjs +10 -59
- package/pp-core/bin/lib/vault.cjs +2570 -950
- package/pp-core/bin/lib/verify.cjs +0 -11
- package/pp-core/bin/portable-timeout.sh +1 -1
- package/pp-core/bin/pp-tools.cjs +157 -54
- package/pp-core/bin/shared/config-defaults.manifest.json +0 -7
- package/pp-core/bin/shared/config-schema.manifest.json +0 -3
- package/pp-core/bin/shared/model-catalog.json +2 -2
- package/pp-core/bin/verify-reapply-patches.cjs +0 -0
- package/pp-core/org-profile.json +26 -0
- package/pp-core/references/autonomous-best-effort.md +8 -7
- package/pp-core/references/loop-hook-dispatch.md +0 -25
- package/pp-core/references/planner-gap-closure.md +0 -1
- package/pp-core/references/planner-tdd.md +0 -1
- package/pp-core/references/planner-vault-standards.md +311 -0
- package/pp-core/references/planning-config.md +0 -8
- package/pp-core/references/tdd.md +0 -1
- package/pp-core/templates/README.md +1 -0
- package/pp-core/templates/VAULT-REVIEW.md +56 -0
- package/pp-core/templates/phase-prompt.md +2 -6
- package/pp-core/workflows/_runtime-launcher.snippet.sh +0 -0
- package/pp-core/workflows/add-tests.md +55 -4
- package/pp-core/workflows/audit-fix.md +63 -2
- package/pp-core/workflows/audit-milestone.md +1 -1
- package/pp-core/workflows/autonomous.md +85 -11
- package/pp-core/workflows/check-todos.md +13 -2
- package/pp-core/workflows/code-review-fix.md +59 -3
- package/pp-core/workflows/code-review.md +10 -57
- package/pp-core/workflows/complete-milestone.md +1 -1
- package/pp-core/workflows/debug.md +54 -4
- package/pp-core/workflows/discovery-sprint/prefill.md +19 -7
- package/pp-core/workflows/discovery-sprint/prototype.md +46 -49
- package/pp-core/workflows/discovery-sprint/session-13-gates.md +2 -0
- package/pp-core/workflows/discovery-sprint/sessions/01-kick-off.md +1 -3
- package/pp-core/workflows/discovery-sprint/sessions/02-as-is.md +1 -3
- package/pp-core/workflows/discovery-sprint/sessions/03-to-be.md +1 -3
- package/pp-core/workflows/discovery-sprint/sessions/04-ux-design.md +20 -24
- package/pp-core/workflows/discovery-sprint/sessions/05-architecture.md +1 -3
- package/pp-core/workflows/discovery-sprint/sessions/06-security.md +1 -3
- package/pp-core/workflows/discovery-sprint/sessions/07-identity.md +1 -3
- package/pp-core/workflows/discovery-sprint/sessions/08-integration.md +1 -3
- package/pp-core/workflows/discovery-sprint/sessions/09-qa.md +1 -3
- package/pp-core/workflows/discovery-sprint/sessions/10-infrastructure.md +1 -6
- package/pp-core/workflows/discovery-sprint/sessions/11-roadmap.md +1 -3
- package/pp-core/workflows/discovery-sprint/sessions/12-delivery-model.md +1 -3
- package/pp-core/workflows/discovery-sprint/sessions/13-wrap-up.md +1 -3
- package/pp-core/workflows/discovery-sprint/templates/context-template.md +1 -1
- package/pp-core/workflows/discovery-sprint/vault-context.md +207 -0
- package/pp-core/workflows/discovery-sprint.md +7 -3
- package/pp-core/workflows/discovery-to-build/steps/architecture-deliverables.md +4 -4
- package/pp-core/workflows/discovery-to-build/templates/pp-handoff.md +43 -1
- package/pp-core/workflows/discovery-to-build.md +10 -2
- package/pp-core/workflows/discuss-phase.md +3 -10
- package/pp-core/workflows/execute-phase/steps/check-query-gate.md +327 -0
- package/pp-core/workflows/execute-phase/steps/client-context-gate.md +3 -2
- package/pp-core/workflows/execute-phase/steps/cross-ai-delegation.md +1 -1
- package/pp-core/workflows/execute-phase/steps/execute-pre-dispatch.md +78 -0
- package/pp-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +94 -0
- package/pp-core/workflows/execute-phase/steps/post-merge-gate.md +6 -8
- package/pp-core/workflows/execute-phase/steps/vault-fetch.md +115 -14
- package/pp-core/workflows/execute-phase/steps/vault-load-gate.md +219 -20
- package/pp-core/workflows/execute-phase/steps/verify-post-gate.md +147 -0
- package/pp-core/workflows/execute-phase.md +152 -128
- package/pp-core/workflows/execute-plan.md +54 -6
- package/pp-core/workflows/fast.md +53 -4
- package/pp-core/workflows/help/modes/full.md +3 -3
- package/pp-core/workflows/init/steps/client-context-capture.md +101 -0
- package/pp-core/workflows/init/steps/gitlab-provisioning.md +130 -0
- package/pp-core/workflows/init/steps/project-md-deferral-narration.md +66 -0
- package/pp-core/workflows/init/steps/reachability-outcome.md +70 -0
- package/pp-core/workflows/init.md +232 -272
- package/pp-core/workflows/new-project.md +31 -23
- package/pp-core/workflows/plan-phase.md +98 -45
- package/pp-core/workflows/quick.md +117 -3
- package/pp-core/workflows/secure-phase.md +1 -1
- package/pp-core/workflows/ship.md +57 -28
- package/pp-core/workflows/undo.md +1 -1
- package/pp-core/workflows/update.md +3 -3
- package/pp-core/workflows/validate-phase.md +58 -3
- package/pp-core/workflows/vault-review.md +161 -23
- package/pp-core/workflows/vault-write-back.md +73 -110
- package/pp-core/workflows/verify-work.md +12 -14
- package/scripts/changeset/cli.cjs +6 -5
- package/scripts/changeset/lint.cjs +0 -0
- package/scripts/changeset/new.cjs +33 -50
- package/scripts/dev-env.cjs +25 -85
- package/scripts/gen-capability-registry.cjs +120 -13
- package/scripts/lib/allowlist-ratchet.cjs +1 -103
- package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -2
- package/scripts/lint-docs-required.cjs +0 -0
- package/scripts/lint-pr-check-project-dir.cjs +1 -3
- package/scripts/lint-test-file-count.allowlist.json +3 -19
- package/scripts/lint-test-file-count.cjs +5 -6
- package/scripts/prompt-injection-scan.sh +19 -17
- package/scripts/release-channel.cjs +0 -0
- package/scripts/sync-manifest-versions.cjs +5 -4
- package/scripts/sync-rulesets.sh +1 -1
- package/scripts/update-size-baseline.cjs +30 -274
- package/scripts/verify-opengsd-sweep.cjs +3 -5
- package/scripts/workflow-size.cjs +9 -61
- package/pp-core/bin/lib/dispatch-plan.cjs +0 -483
- package/pp-core/bin/lib/install-run.cjs +0 -536
- package/pp-core/bin/lib/loop-gates.cjs +0 -213
- package/pp-core/bin/lib/phase-manifest.cjs +0 -527
- package/pp-core/bin/lib/phase-tier.cjs +0 -480
- package/pp-core/bin/lib/plan-graph.cjs +0 -368
- package/pp-core/bin/lib/practices-review-check.cjs +0 -938
- package/pp-core/bin/lib/vault-audit.cjs +0 -2070
- package/pp-core/bin/lib/vault-practices.cjs +0 -1990
- package/pp-core/bin/lib/vault-review.cjs +0 -2359
- package/pp-core/bin/lib/vault-write-back.cjs +0 -877
- package/pp-core/references/executor-vault-trace.md +0 -47
- package/pp-core/references/planner-plan-graph.md +0 -66
- package/pp-core/references/planner-vault-practices.md +0 -112
- package/pp-core/templates/practices/README.md +0 -251
- package/pp-core/templates/practices/error-handling.md +0 -18
- package/pp-core/templates/practices/minimal-comments.md +0 -14
- package/pp-core/templates/practices/naming-convention.md +0 -18
- package/pp-core/templates/practices/practice-template.md +0 -23
- package/pp-core/workflows/discovery-sprint/vault-fetch.md +0 -37
- package/pp-core/workflows/execute-phase/steps/checkpoint-handling.md +0 -45
- package/pp-core/workflows/execute-phase/steps/install-run.md +0 -29
- package/pp-core/workflows/execute-phase/steps/phase-close-rebaseline.md +0 -23
- package/pp-core/workflows/execute-phase/steps/serial-group-dispatch.md +0 -73
- package/pp-core/workflows/execute-phase/steps/vault-door-gate.md +0 -19
- package/pp-core/workflows/plan-phase/steps/phase-tier.md +0 -43
- package/scripts/lib/size-budget.cjs +0 -311
- package/scripts/lint-neutrality-names.cjs +0 -694
- package/scripts/lint-org-coordinates.allowlist.json +0 -35
- package/scripts/lint-org-coordinates.cjs +0 -426
|
@@ -11,12 +11,14 @@
|
|
|
11
11
|
* profile" — that carries a small set of vault/platform coordinates for a
|
|
12
12
|
* single operator's projects. The package name lives entirely in the
|
|
13
13
|
* consuming project's own `.planning/config.json`; pp-core's own source
|
|
14
|
-
* never names one (ORG-01, SC1).
|
|
15
|
-
* pointer resolves nothing
|
|
16
|
-
*
|
|
14
|
+
* never names one (ORG-01, SC1). Upstream, an install that configures no
|
|
15
|
+
* such pointer resolves nothing (ORG-02). This fork reverses ORG-02 on
|
|
16
|
+
* purpose: with no usable pointer it resolves the embedded profile
|
|
17
|
+
* (`EMBEDDED_ORG_PROFILE_NAME`, PBI 74316), and `org.profile: "none"` is the
|
|
18
|
+
* explicit opt-out.
|
|
17
19
|
*
|
|
18
20
|
* It imports ONLY node builtins (`node:fs`, `node:path`) at the top level —
|
|
19
|
-
* no `./vault.cjs`, `./vault-coordinates.cjs`,
|
|
21
|
+
* no `./vault.cjs`, `./vault-coordinates.cjs`,
|
|
20
22
|
* `./io.cjs`, `./shell-command-projection.cjs`. This is load-bearing: in
|
|
21
23
|
* 33-03 BOTH `src/git-provider.cts` and `src/vault-coordinates.cts` import
|
|
22
24
|
* THIS module at their own top level. If this module imported either of
|
|
@@ -31,7 +33,7 @@
|
|
|
31
33
|
* inside `profileValueDisposition`, executed only when a profile value is
|
|
32
34
|
* actually being validated, never at module load. This is the same
|
|
33
35
|
* lazy-require seam `src/vault-command-router.cts` already uses for
|
|
34
|
-
* `./vault.cjs
|
|
36
|
+
* `./vault.cjs`, and it is safe here precisely
|
|
35
37
|
* because `git-provider.cjs` has no top-level dependency on this module —
|
|
36
38
|
* by the time any value is being validated, that module is either already
|
|
37
39
|
* in the require cache or loads cleanly on its own. D-10 requires reusing
|
|
@@ -48,14 +50,17 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
48
50
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
49
51
|
};
|
|
50
52
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
51
|
-
exports.HOST_FORBIDDEN_CHARS = exports.PROFILE_REJECTED_CLONE_TARGET_PATTERNS = exports.ORG_PROFILE_SEED_KEYS = exports.ORG_PROFILE_SEED_SECTION = exports.ORG_PROFILE_ALLOWLIST = exports.
|
|
53
|
+
exports.HOST_FORBIDDEN_CHARS = exports.PROFILE_REJECTED_CLONE_TARGET_PATTERNS = exports.ORG_PROFILE_SEED_KEYS = exports.ORG_PROFILE_INIT_SECTION = exports.ORG_PROFILE_SEED_SECTION = exports.ORG_PROFILE_ALLOWLIST = exports.VAULT_REQUIRED_CONFIG_KEY = exports.EMBEDDED_ORG_PROFILE_ENV = exports.EMBEDDED_ORG_PROFILE_NAME = exports.ORG_PROFILE_NONE = exports.ORG_PROFILE_DATA_FILE = exports.ORG_PROFILE_CONFIG_KEY = void 0;
|
|
54
|
+
exports.embeddedOrgProfilePath = embeddedOrgProfilePath;
|
|
55
|
+
exports.effectiveEmbeddedOrgProfilePath = effectiveEmbeddedOrgProfilePath;
|
|
52
56
|
exports.resolveGlobalPackageRootCandidates = resolveGlobalPackageRootCandidates;
|
|
53
57
|
exports.globalRootCandidatesFor = globalRootCandidatesFor;
|
|
58
|
+
exports.probeOrgProfilePackage = probeOrgProfilePackage;
|
|
54
59
|
exports.resolveOrgProfileDocument = resolveOrgProfileDocument;
|
|
55
60
|
exports.resolveOrgProfile = resolveOrgProfile;
|
|
56
61
|
exports.readProfileValue = readProfileValue;
|
|
62
|
+
exports.booleanPolicyValue = booleanPolicyValue;
|
|
57
63
|
exports.resolveVaultRequired = resolveVaultRequired;
|
|
58
|
-
exports.resolveAcceptedTracePredicates = resolveAcceptedTracePredicates;
|
|
59
64
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
60
65
|
const node_path_1 = __importDefault(require("node:path"));
|
|
61
66
|
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
@@ -66,41 +71,113 @@ exports.ORG_PROFILE_CONFIG_KEY = 'org.profile';
|
|
|
66
71
|
/** The fixed filename read from the resolved package's own directory — never the
|
|
67
72
|
* package's `main`/`exports` entry, and never loaded via `require` (D-08). */
|
|
68
73
|
exports.ORG_PROFILE_DATA_FILE = 'org-profile.json';
|
|
69
|
-
/** The dotted key naming the vault load policy, declared boolean (default
|
|
70
|
-
* false) by `capabilities/vault/capability.json`. Named once here so neither
|
|
71
|
-
* the allowlist, the value guard nor `resolveVaultRequired` restates it. */
|
|
72
|
-
exports.VAULT_REQUIRED_CONFIG_KEY = 'vault.required';
|
|
73
74
|
/**
|
|
74
|
-
* The
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
75
|
+
* The `org.profile` value that opts a project out of every org profile, the
|
|
76
|
+
* embedded one included. Compared case-insensitively after trimming — the
|
|
77
|
+
* same spelling `pp-tools init` already accepts as a skip (UAT-13-F3).
|
|
78
|
+
*/
|
|
79
|
+
exports.ORG_PROFILE_NONE = 'none';
|
|
80
|
+
/**
|
|
81
|
+
* The name `resolveOrgProfileDocument` reports as `packageName` for this
|
|
82
|
+
* fork's embedded profile. Not a valid npm package name shape for a scoped
|
|
83
|
+
* org profile, so it can never collide with a pointer that resolves.
|
|
84
|
+
*
|
|
85
|
+
* FORK-ONLY (PBI 74316). This fork serves a single organisation, so the
|
|
86
|
+
* profile ships inside pp-core as `pp-core/org-profile.json` instead of as a
|
|
87
|
+
* separate package. It carries coordinates only — never a vault ref: the
|
|
88
|
+
* vault version a project builds on is committed per project in its
|
|
89
|
+
* `.platform-bindings.json` (`client.vaultRef`), so no pp-core release can
|
|
90
|
+
* move it.
|
|
80
91
|
*/
|
|
81
|
-
exports.
|
|
82
|
-
/**
|
|
83
|
-
|
|
84
|
-
|
|
92
|
+
exports.EMBEDDED_ORG_PROFILE_NAME = 'embedded';
|
|
93
|
+
/**
|
|
94
|
+
* Absolute path of the embedded profile document. Resolved against this
|
|
95
|
+
* module's own directory: the compiled file sits at `pp-core/bin/lib/`, and
|
|
96
|
+
* both this repo and every installed tree keep `pp-core/org-profile.json`
|
|
97
|
+
* two levels up.
|
|
98
|
+
*/
|
|
99
|
+
function embeddedOrgProfilePath() {
|
|
100
|
+
return node_path_1.default.join(__dirname, '..', '..', exports.ORG_PROFILE_DATA_FILE);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Environment override for the embedded profile, for diagnostics and for test
|
|
104
|
+
* suites that exercise the upstream contract: `none` (or `off`, any case)
|
|
105
|
+
* disables it, any other non-blank value is a path to the document to use
|
|
106
|
+
* instead. Unset or blank leaves the shipped document in place. A project
|
|
107
|
+
* opts out through `org.profile: "none"`, never through this variable.
|
|
108
|
+
*/
|
|
109
|
+
exports.EMBEDDED_ORG_PROFILE_ENV = 'PP_EMBEDDED_ORG_PROFILE';
|
|
110
|
+
/**
|
|
111
|
+
* The embedded document path in force for this process: `opts._embeddedPath`
|
|
112
|
+
* (test seam) when given, else the `EMBEDDED_ORG_PROFILE_ENV` override, else
|
|
113
|
+
* `embeddedOrgProfilePath()`. `null` means no embedded profile applies.
|
|
114
|
+
*/
|
|
115
|
+
function effectiveEmbeddedOrgProfilePath(opts) {
|
|
116
|
+
if (opts && opts._embeddedPath !== undefined)
|
|
117
|
+
return opts._embeddedPath;
|
|
118
|
+
const override = process.env[exports.EMBEDDED_ORG_PROFILE_ENV];
|
|
119
|
+
if (typeof override === 'string' && override.trim() !== '') {
|
|
120
|
+
const trimmed = override.trim();
|
|
121
|
+
return ['none', 'off'].includes(trimmed.toLowerCase()) ? null : trimmed;
|
|
122
|
+
}
|
|
123
|
+
return embeddedOrgProfilePath();
|
|
124
|
+
}
|
|
85
125
|
/**
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
126
|
+
* The version reported for the embedded profile: the pp-core release that
|
|
127
|
+
* ships it. Probes the `VERSION` beside the document (the installer writes
|
|
128
|
+
* `pp-core/VERSION` into every installed tree), then the repo root's. Returns
|
|
129
|
+
* `null` when neither is readable and semver-shaped.
|
|
130
|
+
*/
|
|
131
|
+
function readEmbeddedProfileVersion(dataPath) {
|
|
132
|
+
const dir = node_path_1.default.dirname(dataPath);
|
|
133
|
+
for (const candidate of [node_path_1.default.join(dir, 'VERSION'), node_path_1.default.join(dir, '..', 'VERSION')]) {
|
|
134
|
+
try {
|
|
135
|
+
const raw = node_fs_1.default.readFileSync(candidate, 'utf-8').trim();
|
|
136
|
+
if (/^\d+\.\d+\.\d+/.test(raw))
|
|
137
|
+
return raw;
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
// try the next candidate
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
/** The dotted key naming the vault load policy (02-04, GATE-05 / STRICT-01).
|
|
146
|
+
* Declared in `capabilities/vault/capability.json`'s federated config block,
|
|
147
|
+
* boolean, default `false`. Named here so neither the allowlist, the value
|
|
148
|
+
* guard nor `resolveVaultRequired` hand-restates the string. */
|
|
149
|
+
exports.VAULT_REQUIRED_CONFIG_KEY = 'vault.required';
|
|
150
|
+
/**
|
|
151
|
+
* D-06's fixed allowlist: a profile may contribute exactly these six dotted
|
|
152
|
+
* config keys (five coordinates and one policy key). Entry order is not
|
|
153
|
+
* contractual; membership is. Anything else present in a profile document is
|
|
154
|
+
* dropped with a bounded warning, except the retired keys below.
|
|
155
|
+
*
|
|
156
|
+
* FIVE COORDINATES AND ONE POLICY (02-04, D-VH-4 / STRICT-01). The first five
|
|
157
|
+
* members answer *where to fetch from* — a provider, three shared-vault
|
|
158
|
+
* coordinates and a platform host. `vault.required` answers something
|
|
159
|
+
* categorically different: *whether the fetch is mandatory*. It is the first
|
|
160
|
+
* POLICY member of this list, and it is here deliberately, so one org-profile
|
|
161
|
+
* package can turn the load gate on across a whole fleet without a per-repo
|
|
162
|
+
* edit.
|
|
163
|
+
*
|
|
164
|
+
* That is also this list's sharpest widening, and it is recorded rather than
|
|
165
|
+
* glossed (threat T-02-17): an `npm update` of the profile package can now
|
|
166
|
+
* move the value of a build gate without a code review in the consuming repo.
|
|
167
|
+
* Three things bound it. First, precedence — project config outranks the
|
|
168
|
+
* profile for every member, so any project can opt out explicitly, and
|
|
169
|
+
* `tests/org-profile.test.cjs` pins that direction with a non-vacuity partner
|
|
170
|
+
* rather than assuming it. Second, direction — the profile can only ever make
|
|
171
|
+
* the gate MORE strict from the built-in default of `false`, and a gate
|
|
172
|
+
* turning on is fail-closed. Third, the value guard below accepts only the two
|
|
173
|
+
* boolean literals for this key, so a profile cannot smuggle an arbitrary
|
|
174
|
+
* string into the policy.
|
|
175
|
+
*
|
|
176
|
+
* The warning in the block beneath this one is NOT retracted: it is still
|
|
177
|
+
* exactly right about the seed section, which stays off this list. A key that
|
|
178
|
+
* layers at runtime is a key an `npm update` can silently move — the
|
|
179
|
+
* difference is that for `vault.required` that consequence was weighed and
|
|
180
|
+
* accepted under a locked decision, and for `bindings.*` it was not.
|
|
104
181
|
*/
|
|
105
182
|
exports.ORG_PROFILE_ALLOWLIST = Object.freeze([
|
|
106
183
|
'vault.provider',
|
|
@@ -108,9 +185,15 @@ exports.ORG_PROFILE_ALLOWLIST = Object.freeze([
|
|
|
108
185
|
'vault.fallback_ref',
|
|
109
186
|
'vault.fallback_host',
|
|
110
187
|
'platform.host',
|
|
111
|
-
|
|
112
|
-
exports.VAULT_TRACE_ACCEPTED_PREDICATES_KEY,
|
|
188
|
+
'vault.required',
|
|
113
189
|
]);
|
|
190
|
+
/**
|
|
191
|
+
* Keys a profile written for an older pp-core may still carry, which this
|
|
192
|
+
* engine no longer reads (FORK-ONLY: the vault once had a second, practices
|
|
193
|
+
* mode). They are dropped without a warning: a profile that still says
|
|
194
|
+
* `vault.mode` needs no attention, because the standards vault is the only vault.
|
|
195
|
+
*/
|
|
196
|
+
const RETIRED_PROFILE_KEYS = new Set(['vault.mode', 'vault.practices_enabled']);
|
|
114
197
|
/**
|
|
115
198
|
* The single top-level key naming the Phase 34 init-only seed section.
|
|
116
199
|
*
|
|
@@ -127,6 +210,15 @@ exports.ORG_PROFILE_ALLOWLIST = Object.freeze([
|
|
|
127
210
|
* directly from the package.
|
|
128
211
|
*/
|
|
129
212
|
exports.ORG_PROFILE_SEED_SECTION = 'bindings';
|
|
213
|
+
/**
|
|
214
|
+
* PBI 74299: the top-level key naming the init-answers section — fixed answers
|
|
215
|
+
* for `pp-tools init` questions (`client_name`, `provider`, `visibility`) and
|
|
216
|
+
* `derive_slugs`. Read ONLY by `src/init-contract.cts` through
|
|
217
|
+
* `resolveOrgProfileDocument`; like the seed section it is deliberately absent
|
|
218
|
+
* from `ORG_PROFILE_ALLOWLIST`, because it answers questions once at init and
|
|
219
|
+
* never layers a runtime config key.
|
|
220
|
+
*/
|
|
221
|
+
exports.ORG_PROFILE_INIT_SECTION = 'init';
|
|
130
222
|
/**
|
|
131
223
|
* The five dotted leaf keys the seed section may carry, each prefixed with
|
|
132
224
|
* `ORG_PROFILE_SEED_SECTION`. Membership is contractual; entry ORDER is not.
|
|
@@ -257,39 +349,66 @@ function describeUnknown(value) {
|
|
|
257
349
|
return Object.prototype.toString.call(value);
|
|
258
350
|
}
|
|
259
351
|
}
|
|
260
|
-
// ───
|
|
352
|
+
// ─── resolveGlobalPackageRoot ────────────────────────────────────────────────
|
|
261
353
|
/**
|
|
262
|
-
* D-03's "global" half of the two-path resolution ladder, as a pure
|
|
263
|
-
* with NO subprocess spawn
|
|
264
|
-
*
|
|
354
|
+
* D-03's "global" half of the two-path resolution ladder, as a pure
|
|
355
|
+
* heuristic with NO subprocess spawn (this repo avoids spawning a
|
|
356
|
+
* subprocess for a computation that does not strictly require one).
|
|
357
|
+
* Returns an ORDERED list of existing candidate roots (init-friction F1 —
|
|
358
|
+
* the single-root version silently missed Homebrew layouts):
|
|
265
359
|
*
|
|
266
360
|
* 1. `NPM_CONFIG_PREFIX` / `npm_config_prefix`, when set to a non-blank
|
|
267
361
|
* string: `<prefix>/node_modules` on `win32`, `<prefix>/lib/node_modules`
|
|
268
362
|
* otherwise.
|
|
269
|
-
* 2. Every PATH directory containing a `node` entry, mapped through the
|
|
270
|
-
* `<bin>` → `<global tree>` rule — WITHOUT resolving symlinks.
|
|
271
|
-
*
|
|
272
|
-
* the Cellar, whose `lib/node_modules`
|
|
273
|
-
* real global tree lives at
|
|
274
|
-
* PATH symlink.
|
|
275
|
-
* 3. Derived from `process.execPath`
|
|
363
|
+
* 2. Every PATH directory containing a `node` entry, mapped through the
|
|
364
|
+
* same `<bin>` → `<global tree>` rule — WITHOUT resolving symlinks.
|
|
365
|
+
* This is what fixes Homebrew: `process.execPath` resolves
|
|
366
|
+
* `/opt/homebrew/bin/node` into the Cellar, whose `lib/node_modules`
|
|
367
|
+
* exists but holds only npm itself, while the real global tree lives at
|
|
368
|
+
* `/opt/homebrew/lib/node_modules` beside the PATH symlink.
|
|
369
|
+
* 3. Derived from `process.execPath` (the pre-existing heuristic) — still
|
|
370
|
+
* correct for nvm/Volta layouts.
|
|
276
371
|
*
|
|
277
372
|
* Candidates are deduped and filtered to `fs.existsSync`; resolution tries
|
|
278
|
-
* each in order (first hit wins), so
|
|
279
|
-
* mask a later root that holds the package.
|
|
373
|
+
* each in order (first hit wins), so a wrong-but-existing root earlier in
|
|
374
|
+
* the list cannot mask a later root that actually holds the package.
|
|
375
|
+
*
|
|
376
|
+
* Accepted residual: a layout that honours none of the three sources will
|
|
377
|
+
* still not resolve a globally-installed profile — this degrades per D-09
|
|
378
|
+
* (falls through to the no-profile behaviour with a warning) rather than
|
|
379
|
+
* failing.
|
|
280
380
|
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
*
|
|
381
|
+
* pp-core's own install directory is deliberately NOT a candidate here:
|
|
382
|
+
* the AI-runtime install is a bare directory copy with no `package.json`
|
|
383
|
+
* and no `node_modules` of its own, so resolving relative to it would
|
|
384
|
+
* never find a sibling package.
|
|
285
385
|
*
|
|
286
|
-
*
|
|
287
|
-
*
|
|
386
|
+
* TWO READERS, ONE BUILDER, ONE FLAG (UAT-13-F2). RESOLUTION — the
|
|
387
|
+
* answer-time probe (`probeOrgProfilePackage`) and profile load
|
|
388
|
+
* (`resolveOrgProfileDocument`) — walks the full list above. DETECTION —
|
|
389
|
+
* `buildInitQuestions`' org-profile hint and `verifyInitInvariants`'
|
|
390
|
+
* INIT_QUESTIONS_ORG_PROFILE_UNASKED check in `src/init-contract.cts` —
|
|
391
|
+
* passes `explicitPrefixOnly: true`: when `NPM_CONFIG_PREFIX` /
|
|
392
|
+
* `npm_config_prefix` is set, only the prefix-derived root is scanned.
|
|
393
|
+
* An explicit prefix is the operator's — or a test harness's — statement
|
|
394
|
+
* of where the global tree is, and a hint or a violation must never be
|
|
395
|
+
* produced from a tree the operator did not point at. Detection roots are
|
|
396
|
+
* therefore a SUBSET of resolution roots: anything detected resolves by
|
|
397
|
+
* name, and a package that sits only on a PATH-derived root while an
|
|
398
|
+
* explicit prefix is set is still selectable by typing its name. Without a
|
|
399
|
+
* prefix both readers see the same list, which is what lets detection find
|
|
400
|
+
* a Homebrew layout at all.
|
|
288
401
|
*/
|
|
402
|
+
/** Bounds the PATH scan below — a DoS guard in the same convention as
|
|
403
|
+
* `PROFILE_FLATTEN_LEAF_CAP` (a pathological PATH cannot make resolution
|
|
404
|
+
* scan unboundedly many directories). */
|
|
289
405
|
const GLOBAL_ROOT_PATH_SCAN_CAP = 64;
|
|
290
|
-
/**
|
|
406
|
+
/** The `<bin dir>` → `<global node_modules>` mapping npm itself uses:
|
|
407
|
+
* `<bin>/node_modules` on win32, `<bin>/../lib/node_modules` elsewhere. */
|
|
291
408
|
function globalRootFromBinDir(binDir) {
|
|
292
|
-
return process.platform === 'win32'
|
|
409
|
+
return process.platform === 'win32'
|
|
410
|
+
? node_path_1.default.join(binDir, 'node_modules')
|
|
411
|
+
: node_path_1.default.resolve(binDir, '..', 'lib', 'node_modules');
|
|
293
412
|
}
|
|
294
413
|
function resolveGlobalPackageRootCandidates(opts = {}) {
|
|
295
414
|
const candidates = [];
|
|
@@ -306,16 +425,25 @@ function resolveGlobalPackageRootCandidates(opts = {}) {
|
|
|
306
425
|
}
|
|
307
426
|
});
|
|
308
427
|
try {
|
|
428
|
+
// 1. Explicit prefix always wins — same rule as before.
|
|
309
429
|
const prefixEnv = process.env.NPM_CONFIG_PREFIX ?? process.env.npm_config_prefix;
|
|
310
430
|
if (typeof prefixEnv === 'string' && prefixEnv.trim() !== '') {
|
|
311
431
|
const prefix = prefixEnv.trim();
|
|
312
432
|
push(process.platform === 'win32' ? node_path_1.default.join(prefix, 'node_modules') : node_path_1.default.join(prefix, 'lib', 'node_modules'));
|
|
313
|
-
//
|
|
314
|
-
// nowhere yields [] —
|
|
433
|
+
// Detection mode (see the header): an explicit prefix is authoritative.
|
|
434
|
+
// A prefix that points nowhere yields [] — still the authoritative
|
|
435
|
+
// answer ("no global tree"), never a fall-through to PATH.
|
|
315
436
|
if (opts.explicitPrefixOnly === true)
|
|
316
437
|
return existing();
|
|
317
438
|
}
|
|
318
|
-
|
|
439
|
+
// 2. PATH scan for the directory holding the `node` entry AS INVOKED
|
|
440
|
+
// (symlink not resolved). `process.execPath` resolves symlinks, so a
|
|
441
|
+
// Homebrew layout (`/opt/homebrew/bin/node` → Cellar) lands in the
|
|
442
|
+
// Cellar's private `lib/node_modules` (npm only) and misses the real
|
|
443
|
+
// global tree at `/opt/homebrew/lib/node_modules`. The un-resolved
|
|
444
|
+
// PATH entry is what npm's own prefix derivation is anchored to.
|
|
445
|
+
const pathEnv = process.env.PATH ?? '';
|
|
446
|
+
const pathDirs = pathEnv
|
|
319
447
|
.split(node_path_1.default.delimiter)
|
|
320
448
|
.filter((d) => d.trim() !== '')
|
|
321
449
|
.slice(0, GLOBAL_ROOT_PATH_SCAN_CAP);
|
|
@@ -335,6 +463,9 @@ function resolveGlobalPackageRootCandidates(opts = {}) {
|
|
|
335
463
|
}
|
|
336
464
|
}
|
|
337
465
|
}
|
|
466
|
+
// 3. execPath-derived (the pre-existing heuristic) — still the correct
|
|
467
|
+
// root for nvm/Volta-style layouts where the global tree sits beside
|
|
468
|
+
// the real binary.
|
|
338
469
|
push(globalRootFromBinDir(node_path_1.default.dirname(process.execPath)));
|
|
339
470
|
}
|
|
340
471
|
catch {
|
|
@@ -342,23 +473,12 @@ function resolveGlobalPackageRootCandidates(opts = {}) {
|
|
|
342
473
|
}
|
|
343
474
|
return existing();
|
|
344
475
|
}
|
|
345
|
-
/**
|
|
346
|
-
* Map the `_globalRoot` test seam (a single root, or `null` for "no global
|
|
347
|
-
* root found") onto the candidate-list shape; when the seam is absent, run the
|
|
348
|
-
* production heuristic. The seam wins over `explicitPrefixOnly`.
|
|
349
|
-
*/
|
|
350
|
-
function globalRootCandidatesFor(opts) {
|
|
351
|
-
if (opts && opts._globalRoot !== undefined) {
|
|
352
|
-
return opts._globalRoot === null ? [] : [opts._globalRoot];
|
|
353
|
-
}
|
|
354
|
-
return resolveGlobalPackageRootCandidates({ explicitPrefixOnly: opts?.explicitPrefixOnly === true });
|
|
355
|
-
}
|
|
356
476
|
// ─── resolveProfilePackage ───────────────────────────────────────────────────
|
|
357
477
|
/**
|
|
358
478
|
* D-03's two-path resolution ladder. Attempt `require.resolve(pkgName +
|
|
359
479
|
* '/package.json', { paths: [cwd] })` first and report `source: 'project'`;
|
|
360
|
-
* on failure attempt the same with `paths: [
|
|
361
|
-
*
|
|
480
|
+
* on failure attempt the same with `paths: [globalRoot]` when a global root
|
|
481
|
+
* was found and report `source: 'global'`; otherwise `null`. The
|
|
362
482
|
* project-local hit always wins when both would resolve — the global
|
|
363
483
|
* attempt is only ever reached when the project attempt threw.
|
|
364
484
|
*
|
|
@@ -383,6 +503,37 @@ function resolveProfilePackage(pkgName, cwd, globalRoots) {
|
|
|
383
503
|
}
|
|
384
504
|
return null;
|
|
385
505
|
}
|
|
506
|
+
/** Map the `_globalRoot` test seam (a single root, or `null` for "no global
|
|
507
|
+
* root found") onto the candidate-list shape resolution now uses; when the
|
|
508
|
+
* seam is absent, run the production heuristic. The seam wins over
|
|
509
|
+
* `explicitPrefixOnly`, which only shapes the production heuristic —
|
|
510
|
+
* detection passes it, resolution does not (see the builder's header). */
|
|
511
|
+
function globalRootCandidatesFor(opts) {
|
|
512
|
+
if (opts && opts._globalRoot !== undefined) {
|
|
513
|
+
return opts._globalRoot === null ? [] : [opts._globalRoot];
|
|
514
|
+
}
|
|
515
|
+
return resolveGlobalPackageRootCandidates({ explicitPrefixOnly: opts?.explicitPrefixOnly === true });
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Answer-time probe for `/pp-init`'s `org_profile` question (init-friction
|
|
519
|
+
* F2): does `pkgName` resolve from the project or any global root right
|
|
520
|
+
* now? Returns the probed global roots so a rejection message can name
|
|
521
|
+
* them. Never throws (D-09) — any fault reports `resolved: false` with the
|
|
522
|
+
* roots collected so far.
|
|
523
|
+
*/
|
|
524
|
+
function probeOrgProfilePackage(pkgName, cwd, opts) {
|
|
525
|
+
let probedGlobalRoots = [];
|
|
526
|
+
try {
|
|
527
|
+
probedGlobalRoots = globalRootCandidatesFor(opts);
|
|
528
|
+
const trimmed = typeof pkgName === 'string' ? pkgName.trim() : '';
|
|
529
|
+
if (trimmed === '')
|
|
530
|
+
return { resolved: false, probedGlobalRoots };
|
|
531
|
+
return { resolved: resolveProfilePackage(trimmed, cwd, probedGlobalRoots) !== null, probedGlobalRoots };
|
|
532
|
+
}
|
|
533
|
+
catch {
|
|
534
|
+
return { resolved: false, probedGlobalRoots };
|
|
535
|
+
}
|
|
536
|
+
}
|
|
386
537
|
/**
|
|
387
538
|
* Flatten a parsed profile document to dotted leaf paths, recursing only
|
|
388
539
|
* through plain objects. Bounded on two axes (T-33-06, denial-of-service):
|
|
@@ -428,7 +579,17 @@ function flattenProfileDocument(doc) {
|
|
|
428
579
|
* a DISPOSITION over the shared shape predicates, never as a parallel
|
|
429
580
|
* regex (D-10's "one source of shape truth" requirement).
|
|
430
581
|
*
|
|
431
|
-
*
|
|
582
|
+
* `vault.required` (02-04) is handled BEFORE the scalar guard, because it is
|
|
583
|
+
* the list's only boolean-typed key and the natural JSON an operator writes
|
|
584
|
+
* for it is `true`, not `"true"`. It accepts exactly four inputs — the JSON
|
|
585
|
+
* booleans `true`/`false` and the trimmed strings `'true'`/`'false'` — and
|
|
586
|
+
* carries forward the canonical `'true'`/`'false'` string, so `contributes`
|
|
587
|
+
* stays a `Record<string, string>`. Everything else is rejected, which means
|
|
588
|
+
* a profile cannot smuggle an arbitrary value into a build-gate policy: a
|
|
589
|
+
* typo like `"yes"` is dropped with a bounded warning and the gate stays at
|
|
590
|
+
* its `false` default rather than silently becoming truthy.
|
|
591
|
+
*
|
|
592
|
+
* Scalar guard, applied to every other key: the value must be a string; its
|
|
432
593
|
* trimmed form must be non-empty; the UNTRIMMED value must carry no ASCII
|
|
433
594
|
* control character; the TRIMMED value must carry no whitespace (so
|
|
434
595
|
* leading/trailing padding — e.g. `' azure '` — is tolerated, matching
|
|
@@ -471,9 +632,8 @@ function flattenProfileDocument(doc) {
|
|
|
471
632
|
*/
|
|
472
633
|
function profileValueDisposition(key, value) {
|
|
473
634
|
try {
|
|
474
|
-
//
|
|
475
|
-
// guard below would reject its natural JSON form.
|
|
476
|
-
// string "true" / "false".
|
|
635
|
+
// 02-04: the one boolean-typed allowlist member, disposed of before the
|
|
636
|
+
// string-only scalar guard below would reject its natural JSON form.
|
|
477
637
|
if (key === exports.VAULT_REQUIRED_CONFIG_KEY) {
|
|
478
638
|
if (value === true || value === false)
|
|
479
639
|
return { ok: true, value: String(value) };
|
|
@@ -484,35 +644,16 @@ function profileValueDisposition(key, value) {
|
|
|
484
644
|
}
|
|
485
645
|
return { ok: false, reason: 'value must be the boolean true or false' };
|
|
486
646
|
}
|
|
487
|
-
// The one list-typed member. Carried forward as a canonical JSON array
|
|
488
|
-
// string, so `contributes` stays a string map.
|
|
489
|
-
if (key === exports.VAULT_TRACE_ACCEPTED_PREDICATES_KEY) {
|
|
490
|
-
const list = Array.isArray(value) ? value : null;
|
|
491
|
-
if (list === null || list.length === 0) {
|
|
492
|
-
return { ok: false, reason: 'value must be a non-empty list of https URIs' };
|
|
493
|
-
}
|
|
494
|
-
if (list.length > exports.TRACE_ACCEPTED_PREDICATES_CAP) {
|
|
495
|
-
return { ok: false, reason: `value lists more than ${exports.TRACE_ACCEPTED_PREDICATES_CAP} URIs` };
|
|
496
|
-
}
|
|
497
|
-
const kept = [];
|
|
498
|
-
for (const item of list) {
|
|
499
|
-
if (typeof item !== 'string')
|
|
500
|
-
return { ok: false, reason: 'every entry must be a string' };
|
|
501
|
-
const t = item.trim();
|
|
502
|
-
if (t.length > exports.TRACE_ACCEPTED_PREDICATE_MAX) {
|
|
503
|
-
return { ok: false, reason: `an entry is longer than ${exports.TRACE_ACCEPTED_PREDICATE_MAX} characters` };
|
|
504
|
-
}
|
|
505
|
-
if (!/^https:\/\/[^\s\x00-\x1f\x7f"'<>`\\]+$/.test(t)) {
|
|
506
|
-
return { ok: false, reason: 'every entry must be an https URI with no whitespace or quotes' };
|
|
507
|
-
}
|
|
508
|
-
if (!kept.includes(t))
|
|
509
|
-
kept.push(t);
|
|
510
|
-
}
|
|
511
|
-
return { ok: true, value: JSON.stringify(kept) };
|
|
512
|
-
}
|
|
513
647
|
if (typeof value !== 'string') {
|
|
514
648
|
return { ok: false, reason: 'value must be a string' };
|
|
515
649
|
}
|
|
650
|
+
// PBI 74316: exactly "" for the shared repo is an explicit opt-out of the
|
|
651
|
+
// shared tier — the same decision an empty `vault.fallback_repo` in project
|
|
652
|
+
// config already records (see `resolveSharedCoordinates`). Whitespace is
|
|
653
|
+
// not "", so it still falls through to the blank rejection below.
|
|
654
|
+
if (key === 'vault.fallback_repo' && value === '') {
|
|
655
|
+
return { ok: true, value: '' };
|
|
656
|
+
}
|
|
516
657
|
const trimmed = value.trim();
|
|
517
658
|
if (trimmed === '') {
|
|
518
659
|
return { ok: false, reason: 'value must not be blank' };
|
|
@@ -580,14 +721,23 @@ function profileValueDisposition(key, value) {
|
|
|
580
721
|
* The `_globalRoot` option is the repo's `_`-prefixed test seam convention
|
|
581
722
|
* (as used by `_vault` / `_exec` / `_writeBack` in
|
|
582
723
|
* `src/vault-command-router.cts`); production callers omit it and the
|
|
583
|
-
* module
|
|
724
|
+
* module calls `resolveGlobalPackageRoot()`. Passing `_globalRoot`
|
|
584
725
|
* explicitly (including `null`, to simulate "no global root found")
|
|
585
726
|
* overrides the production heuristic for that one call.
|
|
586
727
|
*
|
|
728
|
+
* FORK-ONLY (PBI 74316) — the embedded profile. Where upstream resolves
|
|
729
|
+
* nothing, this fork resolves `embeddedOrgProfilePath()`: an absent key, a
|
|
730
|
+
* blank or non-string pointer, and a configured package that cannot be found
|
|
731
|
+
* all yield the embedded document (source `embedded`). Only the explicit
|
|
732
|
+
* `ORG_PROFILE_NONE` pointer yields the fully-absent shape, so a project can
|
|
733
|
+
* still opt out. A configured package that DOES resolve always wins. The
|
|
734
|
+
* `_embeddedPath` option is the test seam for that document: a path replaces
|
|
735
|
+
* it, `null` simulates a tree that ships none; without it the
|
|
736
|
+
* `EMBEDDED_ORG_PROFILE_ENV` override applies (`effectiveEmbeddedOrgProfilePath`).
|
|
737
|
+
*
|
|
587
738
|
* Never throws (D-09): any fault anywhere in this function degrades to the
|
|
588
|
-
* fully-absent shape plus one bounded warning
|
|
589
|
-
*
|
|
590
|
-
* `JSON.parse`, never `require` (D-08).
|
|
739
|
+
* fully-absent shape plus one bounded warning. The document is read only via
|
|
740
|
+
* `fs.readFileSync` + `JSON.parse`, never `require` (D-08).
|
|
591
741
|
*/
|
|
592
742
|
function resolveOrgProfileDocument(cwd, opts) {
|
|
593
743
|
const absentDocShape = (warnings = []) => ({
|
|
@@ -600,25 +750,62 @@ function resolveOrgProfileDocument(cwd, opts) {
|
|
|
600
750
|
document: {},
|
|
601
751
|
warnings,
|
|
602
752
|
});
|
|
753
|
+
const embeddedDocShape = (configured) => {
|
|
754
|
+
const dataPath = effectiveEmbeddedOrgProfilePath(opts);
|
|
755
|
+
if (dataPath === null) {
|
|
756
|
+
return { ...absentDocShape(), configured };
|
|
757
|
+
}
|
|
758
|
+
let parsed;
|
|
759
|
+
try {
|
|
760
|
+
parsed = JSON.parse(node_fs_1.default.readFileSync(dataPath, 'utf-8'));
|
|
761
|
+
}
|
|
762
|
+
catch {
|
|
763
|
+
return {
|
|
764
|
+
...absentDocShape([bound('the embedded org profile could not be read — no coordinates contributed.')]),
|
|
765
|
+
configured,
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
if (!isObject(parsed)) {
|
|
769
|
+
return {
|
|
770
|
+
...absentDocShape([bound('the embedded org profile is not a JSON object — no coordinates contributed.')]),
|
|
771
|
+
configured,
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
return {
|
|
775
|
+
configured,
|
|
776
|
+
resolved: true,
|
|
777
|
+
packageName: exports.EMBEDDED_ORG_PROFILE_NAME,
|
|
778
|
+
version: readEmbeddedProfileVersion(dataPath),
|
|
779
|
+
source: 'embedded',
|
|
780
|
+
dataPath,
|
|
781
|
+
document: parsed,
|
|
782
|
+
warnings: [],
|
|
783
|
+
};
|
|
784
|
+
};
|
|
603
785
|
try {
|
|
604
786
|
// Step 1: read the pointer. An absent key (undefined) or a blank string
|
|
605
|
-
//
|
|
606
|
-
//
|
|
607
|
-
//
|
|
608
|
-
// exactly one bounded warning.
|
|
787
|
+
// resolves the embedded profile with ZERO warnings. A present non-string
|
|
788
|
+
// value does the same, plus exactly one bounded warning. The explicit
|
|
789
|
+
// opt-out is the one pointer that resolves nothing.
|
|
609
790
|
const rawPointer = cfgGet(cwd, exports.ORG_PROFILE_CONFIG_KEY, undefined);
|
|
610
791
|
if (rawPointer === undefined) {
|
|
611
|
-
return
|
|
792
|
+
return embeddedDocShape(null);
|
|
612
793
|
}
|
|
613
794
|
if (typeof rawPointer !== 'string') {
|
|
614
795
|
const echoed = sanitizeForWarning(describeUnknown(rawPointer));
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
796
|
+
const embedded = embeddedDocShape(null);
|
|
797
|
+
const consequence = embedded.resolved ? 'ignoring it, the embedded org profile applies.' : 'ignoring, no org profile applied.';
|
|
798
|
+
return {
|
|
799
|
+
...embedded,
|
|
800
|
+
warnings: [bound(`${exports.ORG_PROFILE_CONFIG_KEY} value "${echoed}" is not a string — ${consequence}`), ...embedded.warnings],
|
|
801
|
+
};
|
|
618
802
|
}
|
|
619
803
|
const pkgName = rawPointer.trim();
|
|
620
804
|
if (pkgName === '') {
|
|
621
|
-
return
|
|
805
|
+
return embeddedDocShape(null);
|
|
806
|
+
}
|
|
807
|
+
if (pkgName.toLowerCase() === exports.ORG_PROFILE_NONE) {
|
|
808
|
+
return { ...absentDocShape(), configured: pkgName };
|
|
622
809
|
}
|
|
623
810
|
// Step 2: resolve the package (project, then every global candidate).
|
|
624
811
|
const globalRoots = globalRootCandidatesFor(opts);
|
|
@@ -628,11 +815,13 @@ function resolveOrgProfileDocument(cwd, opts) {
|
|
|
628
815
|
const probed = globalRoots.length > 0
|
|
629
816
|
? `the global npm root(s) ${globalRoots.map((r) => sanitizeForWarning(r)).join(', ')}`
|
|
630
817
|
: 'no locatable global npm root';
|
|
818
|
+
const embedded = embeddedDocShape(pkgName);
|
|
819
|
+
const fallback = embedded.resolved ? 'Falling back to the embedded org profile.' : 'Falling back to built-in defaults.';
|
|
631
820
|
return {
|
|
632
|
-
...
|
|
633
|
-
configured: pkgName,
|
|
821
|
+
...embedded,
|
|
634
822
|
warnings: [
|
|
635
|
-
bound(`org profile "${echoed}" was not found — resolution was attempted from the project and ${probed} and found nothing.
|
|
823
|
+
bound(`org profile "${echoed}" was not found — resolution was attempted from the project and ${probed} and found nothing. ${fallback}`),
|
|
824
|
+
...embedded.warnings,
|
|
636
825
|
],
|
|
637
826
|
};
|
|
638
827
|
}
|
|
@@ -737,6 +926,11 @@ function resolveOrgProfile(cwd, opts) {
|
|
|
737
926
|
if (leaf.key === exports.ORG_PROFILE_SEED_SECTION || leaf.key.startsWith(`${exports.ORG_PROFILE_SEED_SECTION}.`)) {
|
|
738
927
|
continue;
|
|
739
928
|
}
|
|
929
|
+
if (leaf.key === exports.ORG_PROFILE_INIT_SECTION || leaf.key.startsWith(`${exports.ORG_PROFILE_INIT_SECTION}.`)) {
|
|
930
|
+
continue;
|
|
931
|
+
}
|
|
932
|
+
if (RETIRED_PROFILE_KEYS.has(leaf.key))
|
|
933
|
+
continue;
|
|
740
934
|
if (!exports.ORG_PROFILE_ALLOWLIST.includes(leaf.key)) {
|
|
741
935
|
ignored.push(leaf.key);
|
|
742
936
|
if (ignoredWarningCount < PROFILE_IGNORED_WARNING_CAP) {
|
|
@@ -819,52 +1013,99 @@ function readProfileValue(cwd, key, opts) {
|
|
|
819
1013
|
source: resolution.source,
|
|
820
1014
|
};
|
|
821
1015
|
}
|
|
1016
|
+
// ─── resolveVaultRequired (02-04, D-VH-4 / STRICT-01) ────────────────────────
|
|
822
1017
|
/**
|
|
823
|
-
* The
|
|
824
|
-
*
|
|
1018
|
+
* The four inputs a boolean policy value may take, and nothing else — the SAME
|
|
1019
|
+
* disposition `profileValueDisposition` above already applies to this key.
|
|
1020
|
+
* Returns `undefined` for anything else, which is not the same as `false`: it
|
|
1021
|
+
* means "this value expresses no policy".
|
|
825
1022
|
*
|
|
826
|
-
*
|
|
827
|
-
*
|
|
828
|
-
*
|
|
829
|
-
*
|
|
830
|
-
*
|
|
831
|
-
*
|
|
1023
|
+
* 02-SECURITY UF-01. The project-config read used to be
|
|
1024
|
+
* `value === true || value === 'true'`, which collapsed every other spelling
|
|
1025
|
+
* to `false` — while `config-set vault.required TRUE|True|1|yes|on|enabled`
|
|
1026
|
+
* all answered `{"updated": true}` and wrote exactly those, so the supported
|
|
1027
|
+
* CLI could leave a build gate OFF while reporting success. UF-01 closed the
|
|
1028
|
+
* READ half (here); 07-14 closed the WRITE half, and `config-set` now refuses
|
|
1029
|
+
* every one of those spellings with a usage error and writes nothing, so this
|
|
1030
|
+
* function's `undefined` arm is reachable only from a hand-edited config.json
|
|
1031
|
+
* or an org profile. The asymmetry made it sharper: the LOWER-trust org
|
|
1032
|
+
* profile validated to four literals and warned, while project config — which
|
|
1033
|
+
* outranks it — did not, so a project `"TRUE"` both failed to arm the gate AND
|
|
1034
|
+
* suppressed a profile value that would have, because `found` rather than
|
|
1035
|
+
* validity decided precedence.
|
|
832
1036
|
*
|
|
833
|
-
*
|
|
1037
|
+
* Moved here from `src/vault.cts` together with the gate's ladder
|
|
1038
|
+
* (13-REVIEW CR-02), so the gate and the report share one reading.
|
|
834
1039
|
*/
|
|
835
|
-
function
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
if (
|
|
843
|
-
return
|
|
844
|
-
}
|
|
845
|
-
return { required: false, source: 'default', valid: true, raw: undefined, warnings: profile.warnings };
|
|
846
|
-
}
|
|
847
|
-
catch {
|
|
848
|
-
return { required: false, source: 'default', valid: true, raw: undefined, warnings: [] };
|
|
1040
|
+
function booleanPolicyValue(value) {
|
|
1041
|
+
if (value === true || value === false)
|
|
1042
|
+
return value;
|
|
1043
|
+
if (typeof value === 'string') {
|
|
1044
|
+
const trimmed = value.trim();
|
|
1045
|
+
if (trimmed === 'true')
|
|
1046
|
+
return true;
|
|
1047
|
+
if (trimmed === 'false')
|
|
1048
|
+
return false;
|
|
849
1049
|
}
|
|
1050
|
+
return undefined;
|
|
850
1051
|
}
|
|
851
|
-
// ─── resolveAcceptedTracePredicates ──────────────────────────────────────────
|
|
852
1052
|
/**
|
|
853
|
-
*
|
|
854
|
-
*
|
|
855
|
-
*
|
|
856
|
-
*
|
|
857
|
-
*
|
|
1053
|
+
* Resolve the `vault.required` POLICY for `cwd`: project config first, the
|
|
1054
|
+
* org profile second, the registered default (`false`) last. This is the ONE
|
|
1055
|
+
* reader of the policy ladder (13-REVIEW CR-02): the load gate
|
|
1056
|
+
* (`resolveVaultRequiredFlag` in `src/vault.cts`) asks it after its own
|
|
1057
|
+
* `vault.enabled` check, and the `vault org-profile` report asks it for the
|
|
1058
|
+
* `effective` row — so the report and the gate cannot disagree about which
|
|
1059
|
+
* level won for the same inputs. Before CR-02 this function read only the
|
|
1060
|
+
* root `.planning/config.json` and counted only `=== true || === 'true'`,
|
|
1061
|
+
* while the gate read the workstream config first and trimmed, so a project
|
|
1062
|
+
* `" true"` or `"yes"` was reported as overriding a profile `true` that the
|
|
1063
|
+
* gate in fact enforced.
|
|
1064
|
+
*
|
|
1065
|
+
* PROJECT CONFIG OUTRANKS THE PROFILE (T-02-17), so the ordering below is
|
|
1066
|
+
* load-bearing. Level 1 reads RAW config through `resolveConfigKey` with
|
|
1067
|
+
* `config: {}` and `registry: {}`: the workstream config.json, then the root
|
|
1068
|
+
* config.json, each in either dotted form (nested first), with no schema
|
|
1069
|
+
* default. A project value of `false` is a real value and wins. A project
|
|
1070
|
+
* value that is PRESENT BUT UNPARSEABLE (`booleanPolicyValue` answers
|
|
1071
|
+
* `undefined`) does not consume the decision: the profile is consulted as if
|
|
1072
|
+
* the project had said nothing, because a value that expresses no policy must
|
|
1073
|
+
* not outrank one that does. It is returned as `ignoredConfigValue` so the
|
|
1074
|
+
* gate can say so on stderr; this function prints nothing.
|
|
1075
|
+
*
|
|
1076
|
+
* This answers the POLICY, not whether a gate exists: on a project with
|
|
1077
|
+
* `vault.enabled` off no gate renders and `resolveVaultRequiredFlag` answers
|
|
1078
|
+
* `false` whatever this returns.
|
|
1079
|
+
*
|
|
1080
|
+
* Never throws — a failed config read falls through to the profile, and the
|
|
1081
|
+
* profile seam already degrades to a miss on any fault.
|
|
858
1082
|
*/
|
|
859
|
-
function
|
|
1083
|
+
function resolveVaultRequired(cwd, opts) {
|
|
1084
|
+
// Level 1 — project config, raw (ws then root, either dotted form).
|
|
1085
|
+
let ignoredConfigValue;
|
|
1086
|
+
let ignored = false;
|
|
860
1087
|
try {
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
1088
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
1089
|
+
const activation = require('./capability-activation.cjs');
|
|
1090
|
+
const fromConfig = activation.resolveConfigKey(exports.VAULT_REQUIRED_CONFIG_KEY, { config: {}, cwd, registry: {} });
|
|
1091
|
+
if (fromConfig.found) {
|
|
1092
|
+
const decided = booleanPolicyValue(fromConfig.value);
|
|
1093
|
+
if (decided !== undefined)
|
|
1094
|
+
return { required: decided, source: 'config', warnings: [] };
|
|
1095
|
+
ignored = true;
|
|
1096
|
+
ignoredConfigValue = fromConfig.value;
|
|
1097
|
+
}
|
|
866
1098
|
}
|
|
867
|
-
catch {
|
|
868
|
-
|
|
1099
|
+
catch { /* fall through to the profile */ }
|
|
1100
|
+
const withIgnored = (r) => (ignored ? { ...r, ignoredConfigValue } : r);
|
|
1101
|
+
// Level 2 — the org profile. Already narrowed to 'true'/'false' by
|
|
1102
|
+
// profileValueDisposition; booleanPolicyValue agrees with it by construction.
|
|
1103
|
+
const profile = readProfileValue(cwd, exports.VAULT_REQUIRED_CONFIG_KEY, opts);
|
|
1104
|
+
if (profile.value !== undefined) {
|
|
1105
|
+
const decided = booleanPolicyValue(profile.value);
|
|
1106
|
+
if (decided !== undefined)
|
|
1107
|
+
return withIgnored({ required: decided, source: 'profile', warnings: profile.warnings });
|
|
869
1108
|
}
|
|
1109
|
+
// Level 3 — the registered default from capabilities/vault/capability.json.
|
|
1110
|
+
return withIgnored({ required: false, source: 'default', warnings: profile.warnings });
|
|
870
1111
|
}
|