@pareto4/pp-core 1.13.0 → 1.17.1
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 +26 -9
- package/agents/pp-code-reviewer.md +2 -1
- package/agents/pp-debug-session-manager.md +9 -6
- package/agents/pp-executor.md +10 -13
- package/agents/pp-plan-checker.md +4 -4
- package/agents/pp-planner.md +23 -9
- package/agents/pp-vault-reviewer.md +244 -21
- package/agents/pp-verifier.md +7 -3
- package/bin/install.js +10 -3
- package/commands/pp-execute-phase.md +1 -3
- package/commands/pp-ns-review.md +1 -1
- package/commands/pp-plan-phase.md +1 -2
- package/commands/pp-vault-review.md +5 -3
- 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 +14 -19
- 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 +396 -82
- package/pp-core/bin/lib/capability-state.cjs +160 -4
- package/pp-core/bin/lib/capability-writer.cjs +19 -5
- package/pp-core/bin/lib/check-command-router.cjs +3334 -946
- 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 +154 -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 +13 -11
- package/pp-core/bin/lib/git-provider.cjs +4 -4
- package/pp-core/bin/lib/init-command-router.cjs +5 -8
- package/pp-core/bin/lib/init-contract.cjs +648 -738
- package/pp-core/bin/lib/init-interactive.cjs +76 -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 +8 -0
- package/pp-core/bin/lib/loop-resolver.cjs +189 -129
- package/pp-core/bin/lib/org-profile.cjs +469 -152
- 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/practices-review-check.cjs +15 -9
- 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-audit.cjs +650 -126
- package/pp-core/bin/lib/vault-command-router.cjs +2604 -367
- package/pp-core/bin/lib/vault-coordinates.cjs +13 -4
- package/pp-core/bin/lib/vault-index.cjs +699 -0
- package/pp-core/bin/lib/vault-mode.cjs +235 -0
- package/pp-core/bin/lib/vault-practices.cjs +139 -72
- package/pp-core/bin/lib/vault-review.cjs +194 -451
- package/pp-core/bin/lib/vault-seal.cjs +1936 -0
- package/pp-core/bin/lib/vault-standards.cjs +1505 -0
- package/pp-core/bin/lib/vault-trace.cjs +607 -254
- package/pp-core/bin/lib/vault-update.cjs +9 -33
- package/pp-core/bin/lib/vault-write-back.cjs +70 -42
- package/pp-core/bin/lib/vault.cjs +2734 -920
- 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 +184 -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-practices.md +17 -34
- package/pp-core/references/planner-vault-standards.md +320 -0
- package/pp-core/references/planning-config.md +0 -8
- package/pp-core/references/practice-arithmetic-contract.md +121 -0
- package/pp-core/references/tdd.md +0 -1
- package/pp-core/references/verifier-practice-verdicts.md +263 -0
- package/pp-core/templates/README.md +1 -0
- package/pp-core/templates/VAULT-REVIEW.md +202 -0
- package/pp-core/templates/phase-prompt.md +2 -6
- package/pp-core/templates/practices/README.md +20 -23
- 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 -1
- 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 +213 -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 +329 -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 +98 -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 +322 -20
- package/pp-core/workflows/execute-phase/steps/verifier-practice-payload.md +165 -0
- package/pp-core/workflows/execute-phase/steps/verify-post-gate.md +163 -0
- package/pp-core/workflows/execute-phase.md +155 -128
- package/pp-core/workflows/execute-plan.md +56 -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 +282 -22
- 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 +199 -11
- package/scripts/gen-practice-arithmetic-contract.cjs +412 -0
- 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 +12 -10
- package/scripts/practice-arithmetic-contract.allow.json +35 -0
- package/scripts/prompt-injection-scan.sh +22 -17
- package/scripts/record-vault-mode-golden.cjs +89 -0
- 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/vault-calibrate.cjs +996 -0
- 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/references/executor-vault-trace.md +0 -47
- package/pp-core/references/planner-plan-graph.md +0 -66
- 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,9 +11,11 @@
|
|
|
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
21
|
* no `./vault.cjs`, `./vault-coordinates.cjs`, `./vault-write-back.cjs`,
|
|
@@ -39,6 +41,13 @@
|
|
|
39
41
|
* than writing a second regex; the lazy seam is how a zero-import leaf can
|
|
40
42
|
* still do that.
|
|
41
43
|
*
|
|
44
|
+
* The second exception (Vault Assurance 13-03, D-23) is a top-level import
|
|
45
|
+
* of `./vault-mode.cjs`, the mode vocabulary leaf. That module imports
|
|
46
|
+
* nothing at all, so it can never require this module back and cannot close
|
|
47
|
+
* a cycle; it is imported so `resolveVaultMode` and the `vault.mode`
|
|
48
|
+
* disposition quote the key and the two literals from their one owner rather
|
|
49
|
+
* than spelling them here.
|
|
50
|
+
*
|
|
42
51
|
* This file's own prose and code deliberately never name a concrete org
|
|
43
52
|
* profile package or any client — the pointer is described only as "the
|
|
44
53
|
* npm package name an operator configures". `tests/org-profile.test.cjs`
|
|
@@ -48,16 +57,23 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
48
57
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
49
58
|
};
|
|
50
59
|
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.
|
|
60
|
+
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;
|
|
61
|
+
exports.embeddedOrgProfilePath = embeddedOrgProfilePath;
|
|
62
|
+
exports.effectiveEmbeddedOrgProfilePath = effectiveEmbeddedOrgProfilePath;
|
|
52
63
|
exports.resolveGlobalPackageRootCandidates = resolveGlobalPackageRootCandidates;
|
|
53
64
|
exports.globalRootCandidatesFor = globalRootCandidatesFor;
|
|
65
|
+
exports.probeOrgProfilePackage = probeOrgProfilePackage;
|
|
54
66
|
exports.resolveOrgProfileDocument = resolveOrgProfileDocument;
|
|
55
67
|
exports.resolveOrgProfile = resolveOrgProfile;
|
|
56
68
|
exports.readProfileValue = readProfileValue;
|
|
69
|
+
exports.booleanPolicyValue = booleanPolicyValue;
|
|
57
70
|
exports.resolveVaultRequired = resolveVaultRequired;
|
|
58
|
-
exports.
|
|
71
|
+
exports.resolveVaultMode = resolveVaultMode;
|
|
59
72
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
60
73
|
const node_path_1 = __importDefault(require("node:path"));
|
|
74
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
75
|
+
const vaultMode = require("./vault-mode.cjs");
|
|
76
|
+
const { VAULT_MODE_CONFIG_KEY, DEFAULT_VAULT_MODE, VAULT_MODE_REASON, isVaultMode } = vaultMode;
|
|
61
77
|
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
62
78
|
/** The dotted `.planning/config.json` key naming the profile package. Deliberately
|
|
63
79
|
* top-level, NOT nested under `vault.`, because the profile also supplies
|
|
@@ -66,41 +82,113 @@ exports.ORG_PROFILE_CONFIG_KEY = 'org.profile';
|
|
|
66
82
|
/** The fixed filename read from the resolved package's own directory — never the
|
|
67
83
|
* package's `main`/`exports` entry, and never loaded via `require` (D-08). */
|
|
68
84
|
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
85
|
/**
|
|
74
|
-
* The
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
* the traces its earlier engine wrote: the org profile of that engagement
|
|
78
|
-
* lists the earlier engine's predicate URI here. Writers never use it — pp-core
|
|
79
|
-
* always writes its own URI — and pp-core itself names no such URI.
|
|
86
|
+
* The `org.profile` value that opts a project out of every org profile, the
|
|
87
|
+
* embedded one included. Compared case-insensitively after trimming — the
|
|
88
|
+
* same spelling `pp-tools init` already accepts as a skip (UAT-13-F3).
|
|
80
89
|
*/
|
|
81
|
-
exports.
|
|
82
|
-
/**
|
|
83
|
-
|
|
84
|
-
|
|
90
|
+
exports.ORG_PROFILE_NONE = 'none';
|
|
91
|
+
/**
|
|
92
|
+
* The name `resolveOrgProfileDocument` reports as `packageName` for this
|
|
93
|
+
* fork's embedded profile. Not a valid npm package name shape for a scoped
|
|
94
|
+
* org profile, so it can never collide with a pointer that resolves.
|
|
95
|
+
*
|
|
96
|
+
* FORK-ONLY (PBI 74316). This fork serves a single organisation, so the
|
|
97
|
+
* profile ships inside pp-core as `pp-core/org-profile.json` instead of as a
|
|
98
|
+
* separate package. It carries coordinates only — never a vault ref: the
|
|
99
|
+
* vault version a project builds on is committed per project in its
|
|
100
|
+
* `.platform-bindings.json` (`client.vaultRef`), so no pp-core release can
|
|
101
|
+
* move it. Never offered upstream — see docs/PORT-BACK-DELTA.md.
|
|
102
|
+
*/
|
|
103
|
+
exports.EMBEDDED_ORG_PROFILE_NAME = 'embedded';
|
|
104
|
+
/**
|
|
105
|
+
* Absolute path of the embedded profile document. Resolved against this
|
|
106
|
+
* module's own directory: the compiled file sits at `pp-core/bin/lib/`, and
|
|
107
|
+
* both this repo and every installed tree keep `pp-core/org-profile.json`
|
|
108
|
+
* two levels up.
|
|
109
|
+
*/
|
|
110
|
+
function embeddedOrgProfilePath() {
|
|
111
|
+
return node_path_1.default.join(__dirname, '..', '..', exports.ORG_PROFILE_DATA_FILE);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Environment override for the embedded profile, for diagnostics and for test
|
|
115
|
+
* suites that exercise the upstream contract: `none` (or `off`, any case)
|
|
116
|
+
* disables it, any other non-blank value is a path to the document to use
|
|
117
|
+
* instead. Unset or blank leaves the shipped document in place. A project
|
|
118
|
+
* opts out through `org.profile: "none"`, never through this variable.
|
|
119
|
+
*/
|
|
120
|
+
exports.EMBEDDED_ORG_PROFILE_ENV = 'PP_EMBEDDED_ORG_PROFILE';
|
|
121
|
+
/**
|
|
122
|
+
* The embedded document path in force for this process: `opts._embeddedPath`
|
|
123
|
+
* (test seam) when given, else the `EMBEDDED_ORG_PROFILE_ENV` override, else
|
|
124
|
+
* `embeddedOrgProfilePath()`. `null` means no embedded profile applies.
|
|
125
|
+
*/
|
|
126
|
+
function effectiveEmbeddedOrgProfilePath(opts) {
|
|
127
|
+
if (opts && opts._embeddedPath !== undefined)
|
|
128
|
+
return opts._embeddedPath;
|
|
129
|
+
const override = process.env[exports.EMBEDDED_ORG_PROFILE_ENV];
|
|
130
|
+
if (typeof override === 'string' && override.trim() !== '') {
|
|
131
|
+
const trimmed = override.trim();
|
|
132
|
+
return ['none', 'off'].includes(trimmed.toLowerCase()) ? null : trimmed;
|
|
133
|
+
}
|
|
134
|
+
return embeddedOrgProfilePath();
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* The version reported for the embedded profile: the pp-core release that
|
|
138
|
+
* ships it. Probes the `VERSION` beside the document (the installer writes
|
|
139
|
+
* `pp-core/VERSION` into every installed tree), then the repo root's. Returns
|
|
140
|
+
* `null` when neither is readable and semver-shaped.
|
|
141
|
+
*/
|
|
142
|
+
function readEmbeddedProfileVersion(dataPath) {
|
|
143
|
+
const dir = node_path_1.default.dirname(dataPath);
|
|
144
|
+
for (const candidate of [node_path_1.default.join(dir, 'VERSION'), node_path_1.default.join(dir, '..', 'VERSION')]) {
|
|
145
|
+
try {
|
|
146
|
+
const raw = node_fs_1.default.readFileSync(candidate, 'utf-8').trim();
|
|
147
|
+
if (/^\d+\.\d+\.\d+/.test(raw))
|
|
148
|
+
return raw;
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
// try the next candidate
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
/** The dotted key naming the vault load policy (02-04, GATE-05 / STRICT-01).
|
|
157
|
+
* Declared in `capabilities/vault/capability.json`'s federated config block,
|
|
158
|
+
* boolean, default `false`. Named here so neither the allowlist, the value
|
|
159
|
+
* guard nor `resolveVaultRequired` hand-restates the string. */
|
|
160
|
+
exports.VAULT_REQUIRED_CONFIG_KEY = 'vault.required';
|
|
85
161
|
/**
|
|
86
162
|
* D-06's fixed allowlist: a profile may contribute exactly these seven dotted
|
|
87
|
-
* config keys
|
|
163
|
+
* config keys (five coordinates and two policy keys; the seventh, vault.mode,
|
|
164
|
+
* is documented at its entry). Entry order is not contractual; membership is. Anything else
|
|
88
165
|
* present in a profile document is dropped with a bounded warning.
|
|
89
166
|
*
|
|
90
|
-
* FIVE COORDINATES AND ONE POLICY. The first five
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
* `
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
167
|
+
* FIVE COORDINATES AND ONE POLICY (02-04, D-VH-4 / STRICT-01). The first five
|
|
168
|
+
* members answer *where to fetch from* — a provider, three shared-vault
|
|
169
|
+
* coordinates and a platform host. `vault.required` answers something
|
|
170
|
+
* categorically different: *whether the fetch is mandatory*. It is the first
|
|
171
|
+
* POLICY member of this list, and it is here deliberately, so one org-profile
|
|
172
|
+
* package can turn the load gate on across a whole fleet without a per-repo
|
|
173
|
+
* edit.
|
|
174
|
+
*
|
|
175
|
+
* That is also this list's sharpest widening, and it is recorded rather than
|
|
176
|
+
* glossed (threat T-02-17): an `npm update` of the profile package can now
|
|
177
|
+
* move the value of a build gate without a code review in the consuming repo.
|
|
178
|
+
* Three things bound it. First, precedence — project config outranks the
|
|
179
|
+
* profile for every member, so any project can opt out explicitly, and
|
|
180
|
+
* `tests/org-profile.test.cjs` pins that direction with a non-vacuity partner
|
|
181
|
+
* rather than assuming it. Second, direction — the profile can only ever make
|
|
182
|
+
* the gate MORE strict from the built-in default of `false`, and a gate
|
|
183
|
+
* turning on is fail-closed. Third, the value guard below accepts only the two
|
|
184
|
+
* boolean literals for this key, so a profile cannot smuggle an arbitrary
|
|
185
|
+
* string into the policy.
|
|
186
|
+
*
|
|
187
|
+
* The warning in the block beneath this one is NOT retracted: it is still
|
|
188
|
+
* exactly right about the seed section, which stays off this list. A key that
|
|
189
|
+
* layers at runtime is a key an `npm update` can silently move — the
|
|
190
|
+
* difference is that for `vault.required` that consequence was weighed and
|
|
191
|
+
* accepted under a locked decision, and for `bindings.*` it was not.
|
|
104
192
|
*/
|
|
105
193
|
exports.ORG_PROFILE_ALLOWLIST = Object.freeze([
|
|
106
194
|
'vault.provider',
|
|
@@ -108,8 +196,15 @@ exports.ORG_PROFILE_ALLOWLIST = Object.freeze([
|
|
|
108
196
|
'vault.fallback_ref',
|
|
109
197
|
'vault.fallback_host',
|
|
110
198
|
'platform.host',
|
|
111
|
-
|
|
112
|
-
|
|
199
|
+
'vault.required',
|
|
200
|
+
// Vault Assurance 13-03 (D-06): the second POLICY key, after vault.required
|
|
201
|
+
// (02-04). Unlike vault.required, a profile value here can make a gate LESS
|
|
202
|
+
// strict — `standards` turns the blocking vault.audit gate into
|
|
203
|
+
// not_applicable — so the widening is accepted and bounded in 13-CONTEXT
|
|
204
|
+
// D-24 (threat T-13-01): project config outranks the profile, only the two
|
|
205
|
+
// literals pass the disposition below, and the embedded fork profile does
|
|
206
|
+
// not seed it. vault.enabled and init.* stay absent from this list.
|
|
207
|
+
VAULT_MODE_CONFIG_KEY,
|
|
113
208
|
]);
|
|
114
209
|
/**
|
|
115
210
|
* The single top-level key naming the Phase 34 init-only seed section.
|
|
@@ -127,6 +222,15 @@ exports.ORG_PROFILE_ALLOWLIST = Object.freeze([
|
|
|
127
222
|
* directly from the package.
|
|
128
223
|
*/
|
|
129
224
|
exports.ORG_PROFILE_SEED_SECTION = 'bindings';
|
|
225
|
+
/**
|
|
226
|
+
* PBI 74299: the top-level key naming the init-answers section — fixed answers
|
|
227
|
+
* for `pp-tools init` questions (`client_name`, `provider`, `visibility`) and
|
|
228
|
+
* `derive_slugs`. Read ONLY by `src/init-contract.cts` through
|
|
229
|
+
* `resolveOrgProfileDocument`; like the seed section it is deliberately absent
|
|
230
|
+
* from `ORG_PROFILE_ALLOWLIST`, because it answers questions once at init and
|
|
231
|
+
* never layers a runtime config key.
|
|
232
|
+
*/
|
|
233
|
+
exports.ORG_PROFILE_INIT_SECTION = 'init';
|
|
130
234
|
/**
|
|
131
235
|
* The five dotted leaf keys the seed section may carry, each prefixed with
|
|
132
236
|
* `ORG_PROFILE_SEED_SECTION`. Membership is contractual; entry ORDER is not.
|
|
@@ -257,39 +361,66 @@ function describeUnknown(value) {
|
|
|
257
361
|
return Object.prototype.toString.call(value);
|
|
258
362
|
}
|
|
259
363
|
}
|
|
260
|
-
// ───
|
|
364
|
+
// ─── resolveGlobalPackageRoot ────────────────────────────────────────────────
|
|
261
365
|
/**
|
|
262
|
-
* D-03's "global" half of the two-path resolution ladder, as a pure
|
|
263
|
-
* with NO subprocess spawn
|
|
264
|
-
*
|
|
366
|
+
* D-03's "global" half of the two-path resolution ladder, as a pure
|
|
367
|
+
* heuristic with NO subprocess spawn (this repo avoids spawning a
|
|
368
|
+
* subprocess for a computation that does not strictly require one).
|
|
369
|
+
* Returns an ORDERED list of existing candidate roots (init-friction F1 —
|
|
370
|
+
* the single-root version silently missed Homebrew layouts):
|
|
265
371
|
*
|
|
266
372
|
* 1. `NPM_CONFIG_PREFIX` / `npm_config_prefix`, when set to a non-blank
|
|
267
373
|
* string: `<prefix>/node_modules` on `win32`, `<prefix>/lib/node_modules`
|
|
268
374
|
* 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`
|
|
375
|
+
* 2. Every PATH directory containing a `node` entry, mapped through the
|
|
376
|
+
* same `<bin>` → `<global tree>` rule — WITHOUT resolving symlinks.
|
|
377
|
+
* This is what fixes Homebrew: `process.execPath` resolves
|
|
378
|
+
* `/opt/homebrew/bin/node` into the Cellar, whose `lib/node_modules`
|
|
379
|
+
* exists but holds only npm itself, while the real global tree lives at
|
|
380
|
+
* `/opt/homebrew/lib/node_modules` beside the PATH symlink.
|
|
381
|
+
* 3. Derived from `process.execPath` (the pre-existing heuristic) — still
|
|
382
|
+
* correct for nvm/Volta layouts.
|
|
276
383
|
*
|
|
277
384
|
* 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.
|
|
385
|
+
* each in order (first hit wins), so a wrong-but-existing root earlier in
|
|
386
|
+
* the list cannot mask a later root that actually holds the package.
|
|
387
|
+
*
|
|
388
|
+
* Accepted residual: a layout that honours none of the three sources will
|
|
389
|
+
* still not resolve a globally-installed profile — this degrades per D-09
|
|
390
|
+
* (falls through to the no-profile behaviour with a warning) rather than
|
|
391
|
+
* failing.
|
|
280
392
|
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
*
|
|
393
|
+
* pp-core's own install directory is deliberately NOT a candidate here:
|
|
394
|
+
* the AI-runtime install is a bare directory copy with no `package.json`
|
|
395
|
+
* and no `node_modules` of its own, so resolving relative to it would
|
|
396
|
+
* never find a sibling package.
|
|
285
397
|
*
|
|
286
|
-
*
|
|
287
|
-
*
|
|
398
|
+
* TWO READERS, ONE BUILDER, ONE FLAG (UAT-13-F2). RESOLUTION — the
|
|
399
|
+
* answer-time probe (`probeOrgProfilePackage`) and profile load
|
|
400
|
+
* (`resolveOrgProfileDocument`) — walks the full list above. DETECTION —
|
|
401
|
+
* `buildInitQuestions`' org-profile hint and `verifyInitInvariants`'
|
|
402
|
+
* INIT_QUESTIONS_ORG_PROFILE_UNASKED check in `src/init-contract.cts` —
|
|
403
|
+
* passes `explicitPrefixOnly: true`: when `NPM_CONFIG_PREFIX` /
|
|
404
|
+
* `npm_config_prefix` is set, only the prefix-derived root is scanned.
|
|
405
|
+
* An explicit prefix is the operator's — or a test harness's — statement
|
|
406
|
+
* of where the global tree is, and a hint or a violation must never be
|
|
407
|
+
* produced from a tree the operator did not point at. Detection roots are
|
|
408
|
+
* therefore a SUBSET of resolution roots: anything detected resolves by
|
|
409
|
+
* name, and a package that sits only on a PATH-derived root while an
|
|
410
|
+
* explicit prefix is set is still selectable by typing its name. Without a
|
|
411
|
+
* prefix both readers see the same list, which is what lets detection find
|
|
412
|
+
* a Homebrew layout at all.
|
|
288
413
|
*/
|
|
414
|
+
/** Bounds the PATH scan below — a DoS guard in the same convention as
|
|
415
|
+
* `PROFILE_FLATTEN_LEAF_CAP` (a pathological PATH cannot make resolution
|
|
416
|
+
* scan unboundedly many directories). */
|
|
289
417
|
const GLOBAL_ROOT_PATH_SCAN_CAP = 64;
|
|
290
|
-
/**
|
|
418
|
+
/** The `<bin dir>` → `<global node_modules>` mapping npm itself uses:
|
|
419
|
+
* `<bin>/node_modules` on win32, `<bin>/../lib/node_modules` elsewhere. */
|
|
291
420
|
function globalRootFromBinDir(binDir) {
|
|
292
|
-
return process.platform === 'win32'
|
|
421
|
+
return process.platform === 'win32'
|
|
422
|
+
? node_path_1.default.join(binDir, 'node_modules')
|
|
423
|
+
: node_path_1.default.resolve(binDir, '..', 'lib', 'node_modules');
|
|
293
424
|
}
|
|
294
425
|
function resolveGlobalPackageRootCandidates(opts = {}) {
|
|
295
426
|
const candidates = [];
|
|
@@ -306,16 +437,25 @@ function resolveGlobalPackageRootCandidates(opts = {}) {
|
|
|
306
437
|
}
|
|
307
438
|
});
|
|
308
439
|
try {
|
|
440
|
+
// 1. Explicit prefix always wins — same rule as before.
|
|
309
441
|
const prefixEnv = process.env.NPM_CONFIG_PREFIX ?? process.env.npm_config_prefix;
|
|
310
442
|
if (typeof prefixEnv === 'string' && prefixEnv.trim() !== '') {
|
|
311
443
|
const prefix = prefixEnv.trim();
|
|
312
444
|
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 [] —
|
|
445
|
+
// Detection mode (see the header): an explicit prefix is authoritative.
|
|
446
|
+
// A prefix that points nowhere yields [] — still the authoritative
|
|
447
|
+
// answer ("no global tree"), never a fall-through to PATH.
|
|
315
448
|
if (opts.explicitPrefixOnly === true)
|
|
316
449
|
return existing();
|
|
317
450
|
}
|
|
318
|
-
|
|
451
|
+
// 2. PATH scan for the directory holding the `node` entry AS INVOKED
|
|
452
|
+
// (symlink not resolved). `process.execPath` resolves symlinks, so a
|
|
453
|
+
// Homebrew layout (`/opt/homebrew/bin/node` → Cellar) lands in the
|
|
454
|
+
// Cellar's private `lib/node_modules` (npm only) and misses the real
|
|
455
|
+
// global tree at `/opt/homebrew/lib/node_modules`. The un-resolved
|
|
456
|
+
// PATH entry is what npm's own prefix derivation is anchored to.
|
|
457
|
+
const pathEnv = process.env.PATH ?? '';
|
|
458
|
+
const pathDirs = pathEnv
|
|
319
459
|
.split(node_path_1.default.delimiter)
|
|
320
460
|
.filter((d) => d.trim() !== '')
|
|
321
461
|
.slice(0, GLOBAL_ROOT_PATH_SCAN_CAP);
|
|
@@ -335,6 +475,9 @@ function resolveGlobalPackageRootCandidates(opts = {}) {
|
|
|
335
475
|
}
|
|
336
476
|
}
|
|
337
477
|
}
|
|
478
|
+
// 3. execPath-derived (the pre-existing heuristic) — still the correct
|
|
479
|
+
// root for nvm/Volta-style layouts where the global tree sits beside
|
|
480
|
+
// the real binary.
|
|
338
481
|
push(globalRootFromBinDir(node_path_1.default.dirname(process.execPath)));
|
|
339
482
|
}
|
|
340
483
|
catch {
|
|
@@ -342,23 +485,12 @@ function resolveGlobalPackageRootCandidates(opts = {}) {
|
|
|
342
485
|
}
|
|
343
486
|
return existing();
|
|
344
487
|
}
|
|
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
488
|
// ─── resolveProfilePackage ───────────────────────────────────────────────────
|
|
357
489
|
/**
|
|
358
490
|
* D-03's two-path resolution ladder. Attempt `require.resolve(pkgName +
|
|
359
491
|
* '/package.json', { paths: [cwd] })` first and report `source: 'project'`;
|
|
360
|
-
* on failure attempt the same with `paths: [
|
|
361
|
-
*
|
|
492
|
+
* on failure attempt the same with `paths: [globalRoot]` when a global root
|
|
493
|
+
* was found and report `source: 'global'`; otherwise `null`. The
|
|
362
494
|
* project-local hit always wins when both would resolve — the global
|
|
363
495
|
* attempt is only ever reached when the project attempt threw.
|
|
364
496
|
*
|
|
@@ -383,6 +515,37 @@ function resolveProfilePackage(pkgName, cwd, globalRoots) {
|
|
|
383
515
|
}
|
|
384
516
|
return null;
|
|
385
517
|
}
|
|
518
|
+
/** Map the `_globalRoot` test seam (a single root, or `null` for "no global
|
|
519
|
+
* root found") onto the candidate-list shape resolution now uses; when the
|
|
520
|
+
* seam is absent, run the production heuristic. The seam wins over
|
|
521
|
+
* `explicitPrefixOnly`, which only shapes the production heuristic —
|
|
522
|
+
* detection passes it, resolution does not (see the builder's header). */
|
|
523
|
+
function globalRootCandidatesFor(opts) {
|
|
524
|
+
if (opts && opts._globalRoot !== undefined) {
|
|
525
|
+
return opts._globalRoot === null ? [] : [opts._globalRoot];
|
|
526
|
+
}
|
|
527
|
+
return resolveGlobalPackageRootCandidates({ explicitPrefixOnly: opts?.explicitPrefixOnly === true });
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Answer-time probe for `/pp-init`'s `org_profile` question (init-friction
|
|
531
|
+
* F2): does `pkgName` resolve from the project or any global root right
|
|
532
|
+
* now? Returns the probed global roots so a rejection message can name
|
|
533
|
+
* them. Never throws (D-09) — any fault reports `resolved: false` with the
|
|
534
|
+
* roots collected so far.
|
|
535
|
+
*/
|
|
536
|
+
function probeOrgProfilePackage(pkgName, cwd, opts) {
|
|
537
|
+
let probedGlobalRoots = [];
|
|
538
|
+
try {
|
|
539
|
+
probedGlobalRoots = globalRootCandidatesFor(opts);
|
|
540
|
+
const trimmed = typeof pkgName === 'string' ? pkgName.trim() : '';
|
|
541
|
+
if (trimmed === '')
|
|
542
|
+
return { resolved: false, probedGlobalRoots };
|
|
543
|
+
return { resolved: resolveProfilePackage(trimmed, cwd, probedGlobalRoots) !== null, probedGlobalRoots };
|
|
544
|
+
}
|
|
545
|
+
catch {
|
|
546
|
+
return { resolved: false, probedGlobalRoots };
|
|
547
|
+
}
|
|
548
|
+
}
|
|
386
549
|
/**
|
|
387
550
|
* Flatten a parsed profile document to dotted leaf paths, recursing only
|
|
388
551
|
* through plain objects. Bounded on two axes (T-33-06, denial-of-service):
|
|
@@ -428,7 +591,17 @@ function flattenProfileDocument(doc) {
|
|
|
428
591
|
* a DISPOSITION over the shared shape predicates, never as a parallel
|
|
429
592
|
* regex (D-10's "one source of shape truth" requirement).
|
|
430
593
|
*
|
|
431
|
-
*
|
|
594
|
+
* `vault.required` (02-04) is handled BEFORE the scalar guard, because it is
|
|
595
|
+
* the list's only boolean-typed key and the natural JSON an operator writes
|
|
596
|
+
* for it is `true`, not `"true"`. It accepts exactly four inputs — the JSON
|
|
597
|
+
* booleans `true`/`false` and the trimmed strings `'true'`/`'false'` — and
|
|
598
|
+
* carries forward the canonical `'true'`/`'false'` string, so `contributes`
|
|
599
|
+
* stays a `Record<string, string>`. Everything else is rejected, which means
|
|
600
|
+
* a profile cannot smuggle an arbitrary value into a build-gate policy: a
|
|
601
|
+
* typo like `"yes"` is dropped with a bounded warning and the gate stays at
|
|
602
|
+
* its `false` default rather than silently becoming truthy.
|
|
603
|
+
*
|
|
604
|
+
* Scalar guard, applied to every other key: the value must be a string; its
|
|
432
605
|
* trimmed form must be non-empty; the UNTRIMMED value must carry no ASCII
|
|
433
606
|
* control character; the TRIMMED value must carry no whitespace (so
|
|
434
607
|
* leading/trailing padding — e.g. `' azure '` — is tolerated, matching
|
|
@@ -471,9 +644,8 @@ function flattenProfileDocument(doc) {
|
|
|
471
644
|
*/
|
|
472
645
|
function profileValueDisposition(key, value) {
|
|
473
646
|
try {
|
|
474
|
-
//
|
|
475
|
-
// guard below would reject its natural JSON form.
|
|
476
|
-
// string "true" / "false".
|
|
647
|
+
// 02-04: the one boolean-typed allowlist member, disposed of before the
|
|
648
|
+
// string-only scalar guard below would reject its natural JSON form.
|
|
477
649
|
if (key === exports.VAULT_REQUIRED_CONFIG_KEY) {
|
|
478
650
|
if (value === true || value === false)
|
|
479
651
|
return { ok: true, value: String(value) };
|
|
@@ -484,35 +656,25 @@ function profileValueDisposition(key, value) {
|
|
|
484
656
|
}
|
|
485
657
|
return { ok: false, reason: 'value must be the boolean true or false' };
|
|
486
658
|
}
|
|
487
|
-
//
|
|
488
|
-
//
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
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) };
|
|
659
|
+
// 13-03 (D-06, D-09): the mode accepts only its two literals, trimmed.
|
|
660
|
+
// Anything else is dropped through the existing warning / rejected[]
|
|
661
|
+
// path, and the mode then resolves to the default. The reason names the
|
|
662
|
+
// allowed values and never echoes the rejected one.
|
|
663
|
+
if (key === VAULT_MODE_CONFIG_KEY) {
|
|
664
|
+
if (isVaultMode(value))
|
|
665
|
+
return { ok: true, value: value.trim() };
|
|
666
|
+
return { ok: false, reason: 'value must be practices or standards' };
|
|
512
667
|
}
|
|
513
668
|
if (typeof value !== 'string') {
|
|
514
669
|
return { ok: false, reason: 'value must be a string' };
|
|
515
670
|
}
|
|
671
|
+
// PBI 74316: exactly "" for the shared repo is an explicit opt-out of the
|
|
672
|
+
// shared tier — the same decision an empty `vault.fallback_repo` in project
|
|
673
|
+
// config already records (see `resolveSharedCoordinates`). Whitespace is
|
|
674
|
+
// not "", so it still falls through to the blank rejection below.
|
|
675
|
+
if (key === 'vault.fallback_repo' && value === '') {
|
|
676
|
+
return { ok: true, value: '' };
|
|
677
|
+
}
|
|
516
678
|
const trimmed = value.trim();
|
|
517
679
|
if (trimmed === '') {
|
|
518
680
|
return { ok: false, reason: 'value must not be blank' };
|
|
@@ -580,14 +742,23 @@ function profileValueDisposition(key, value) {
|
|
|
580
742
|
* The `_globalRoot` option is the repo's `_`-prefixed test seam convention
|
|
581
743
|
* (as used by `_vault` / `_exec` / `_writeBack` in
|
|
582
744
|
* `src/vault-command-router.cts`); production callers omit it and the
|
|
583
|
-
* module
|
|
745
|
+
* module calls `resolveGlobalPackageRoot()`. Passing `_globalRoot`
|
|
584
746
|
* explicitly (including `null`, to simulate "no global root found")
|
|
585
747
|
* overrides the production heuristic for that one call.
|
|
586
748
|
*
|
|
749
|
+
* FORK-ONLY (PBI 74316) — the embedded profile. Where upstream resolves
|
|
750
|
+
* nothing, this fork resolves `embeddedOrgProfilePath()`: an absent key, a
|
|
751
|
+
* blank or non-string pointer, and a configured package that cannot be found
|
|
752
|
+
* all yield the embedded document (source `embedded`). Only the explicit
|
|
753
|
+
* `ORG_PROFILE_NONE` pointer yields the fully-absent shape, so a project can
|
|
754
|
+
* still opt out. A configured package that DOES resolve always wins. The
|
|
755
|
+
* `_embeddedPath` option is the test seam for that document: a path replaces
|
|
756
|
+
* it, `null` simulates a tree that ships none; without it the
|
|
757
|
+
* `EMBEDDED_ORG_PROFILE_ENV` override applies (`effectiveEmbeddedOrgProfilePath`).
|
|
758
|
+
*
|
|
587
759
|
* 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).
|
|
760
|
+
* fully-absent shape plus one bounded warning. The document is read only via
|
|
761
|
+
* `fs.readFileSync` + `JSON.parse`, never `require` (D-08).
|
|
591
762
|
*/
|
|
592
763
|
function resolveOrgProfileDocument(cwd, opts) {
|
|
593
764
|
const absentDocShape = (warnings = []) => ({
|
|
@@ -600,25 +771,62 @@ function resolveOrgProfileDocument(cwd, opts) {
|
|
|
600
771
|
document: {},
|
|
601
772
|
warnings,
|
|
602
773
|
});
|
|
774
|
+
const embeddedDocShape = (configured) => {
|
|
775
|
+
const dataPath = effectiveEmbeddedOrgProfilePath(opts);
|
|
776
|
+
if (dataPath === null) {
|
|
777
|
+
return { ...absentDocShape(), configured };
|
|
778
|
+
}
|
|
779
|
+
let parsed;
|
|
780
|
+
try {
|
|
781
|
+
parsed = JSON.parse(node_fs_1.default.readFileSync(dataPath, 'utf-8'));
|
|
782
|
+
}
|
|
783
|
+
catch {
|
|
784
|
+
return {
|
|
785
|
+
...absentDocShape([bound('the embedded org profile could not be read — no coordinates contributed.')]),
|
|
786
|
+
configured,
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
if (!isObject(parsed)) {
|
|
790
|
+
return {
|
|
791
|
+
...absentDocShape([bound('the embedded org profile is not a JSON object — no coordinates contributed.')]),
|
|
792
|
+
configured,
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
return {
|
|
796
|
+
configured,
|
|
797
|
+
resolved: true,
|
|
798
|
+
packageName: exports.EMBEDDED_ORG_PROFILE_NAME,
|
|
799
|
+
version: readEmbeddedProfileVersion(dataPath),
|
|
800
|
+
source: 'embedded',
|
|
801
|
+
dataPath,
|
|
802
|
+
document: parsed,
|
|
803
|
+
warnings: [],
|
|
804
|
+
};
|
|
805
|
+
};
|
|
603
806
|
try {
|
|
604
807
|
// Step 1: read the pointer. An absent key (undefined) or a blank string
|
|
605
|
-
//
|
|
606
|
-
//
|
|
607
|
-
//
|
|
608
|
-
// exactly one bounded warning.
|
|
808
|
+
// resolves the embedded profile with ZERO warnings. A present non-string
|
|
809
|
+
// value does the same, plus exactly one bounded warning. The explicit
|
|
810
|
+
// opt-out is the one pointer that resolves nothing.
|
|
609
811
|
const rawPointer = cfgGet(cwd, exports.ORG_PROFILE_CONFIG_KEY, undefined);
|
|
610
812
|
if (rawPointer === undefined) {
|
|
611
|
-
return
|
|
813
|
+
return embeddedDocShape(null);
|
|
612
814
|
}
|
|
613
815
|
if (typeof rawPointer !== 'string') {
|
|
614
816
|
const echoed = sanitizeForWarning(describeUnknown(rawPointer));
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
817
|
+
const embedded = embeddedDocShape(null);
|
|
818
|
+
const consequence = embedded.resolved ? 'ignoring it, the embedded org profile applies.' : 'ignoring, no org profile applied.';
|
|
819
|
+
return {
|
|
820
|
+
...embedded,
|
|
821
|
+
warnings: [bound(`${exports.ORG_PROFILE_CONFIG_KEY} value "${echoed}" is not a string — ${consequence}`), ...embedded.warnings],
|
|
822
|
+
};
|
|
618
823
|
}
|
|
619
824
|
const pkgName = rawPointer.trim();
|
|
620
825
|
if (pkgName === '') {
|
|
621
|
-
return
|
|
826
|
+
return embeddedDocShape(null);
|
|
827
|
+
}
|
|
828
|
+
if (pkgName.toLowerCase() === exports.ORG_PROFILE_NONE) {
|
|
829
|
+
return { ...absentDocShape(), configured: pkgName };
|
|
622
830
|
}
|
|
623
831
|
// Step 2: resolve the package (project, then every global candidate).
|
|
624
832
|
const globalRoots = globalRootCandidatesFor(opts);
|
|
@@ -628,11 +836,13 @@ function resolveOrgProfileDocument(cwd, opts) {
|
|
|
628
836
|
const probed = globalRoots.length > 0
|
|
629
837
|
? `the global npm root(s) ${globalRoots.map((r) => sanitizeForWarning(r)).join(', ')}`
|
|
630
838
|
: 'no locatable global npm root';
|
|
839
|
+
const embedded = embeddedDocShape(pkgName);
|
|
840
|
+
const fallback = embedded.resolved ? 'Falling back to the embedded org profile.' : 'Falling back to built-in defaults.';
|
|
631
841
|
return {
|
|
632
|
-
...
|
|
633
|
-
configured: pkgName,
|
|
842
|
+
...embedded,
|
|
634
843
|
warnings: [
|
|
635
|
-
bound(`org profile "${echoed}" was not found — resolution was attempted from the project and ${probed} and found nothing.
|
|
844
|
+
bound(`org profile "${echoed}" was not found — resolution was attempted from the project and ${probed} and found nothing. ${fallback}`),
|
|
845
|
+
...embedded.warnings,
|
|
636
846
|
],
|
|
637
847
|
};
|
|
638
848
|
}
|
|
@@ -737,6 +947,9 @@ function resolveOrgProfile(cwd, opts) {
|
|
|
737
947
|
if (leaf.key === exports.ORG_PROFILE_SEED_SECTION || leaf.key.startsWith(`${exports.ORG_PROFILE_SEED_SECTION}.`)) {
|
|
738
948
|
continue;
|
|
739
949
|
}
|
|
950
|
+
if (leaf.key === exports.ORG_PROFILE_INIT_SECTION || leaf.key.startsWith(`${exports.ORG_PROFILE_INIT_SECTION}.`)) {
|
|
951
|
+
continue;
|
|
952
|
+
}
|
|
740
953
|
if (!exports.ORG_PROFILE_ALLOWLIST.includes(leaf.key)) {
|
|
741
954
|
ignored.push(leaf.key);
|
|
742
955
|
if (ignoredWarningCount < PROFILE_IGNORED_WARNING_CAP) {
|
|
@@ -819,52 +1032,156 @@ function readProfileValue(cwd, key, opts) {
|
|
|
819
1032
|
source: resolution.source,
|
|
820
1033
|
};
|
|
821
1034
|
}
|
|
1035
|
+
// ─── resolveVaultRequired (02-04, D-VH-4 / STRICT-01) ────────────────────────
|
|
1036
|
+
/**
|
|
1037
|
+
* The four inputs a boolean policy value may take, and nothing else — the SAME
|
|
1038
|
+
* disposition `profileValueDisposition` above already applies to this key.
|
|
1039
|
+
* Returns `undefined` for anything else, which is not the same as `false`: it
|
|
1040
|
+
* means "this value expresses no policy".
|
|
1041
|
+
*
|
|
1042
|
+
* 02-SECURITY UF-01. The project-config read used to be
|
|
1043
|
+
* `value === true || value === 'true'`, which collapsed every other spelling
|
|
1044
|
+
* to `false` — while `config-set vault.required TRUE|True|1|yes|on|enabled`
|
|
1045
|
+
* all answered `{"updated": true}` and wrote exactly those, so the supported
|
|
1046
|
+
* CLI could leave a build gate OFF while reporting success. UF-01 closed the
|
|
1047
|
+
* READ half (here); 07-14 closed the WRITE half, and `config-set` now refuses
|
|
1048
|
+
* every one of those spellings with a usage error and writes nothing, so this
|
|
1049
|
+
* function's `undefined` arm is reachable only from a hand-edited config.json
|
|
1050
|
+
* or an org profile. The asymmetry made it sharper: the LOWER-trust org
|
|
1051
|
+
* profile validated to four literals and warned, while project config — which
|
|
1052
|
+
* outranks it — did not, so a project `"TRUE"` both failed to arm the gate AND
|
|
1053
|
+
* suppressed a profile value that would have, because `found` rather than
|
|
1054
|
+
* validity decided precedence.
|
|
1055
|
+
*
|
|
1056
|
+
* Moved here from `src/vault.cts` together with the gate's ladder
|
|
1057
|
+
* (13-REVIEW CR-02), so the gate and the report share one reading.
|
|
1058
|
+
*/
|
|
1059
|
+
function booleanPolicyValue(value) {
|
|
1060
|
+
if (value === true || value === false)
|
|
1061
|
+
return value;
|
|
1062
|
+
if (typeof value === 'string') {
|
|
1063
|
+
const trimmed = value.trim();
|
|
1064
|
+
if (trimmed === 'true')
|
|
1065
|
+
return true;
|
|
1066
|
+
if (trimmed === 'false')
|
|
1067
|
+
return false;
|
|
1068
|
+
}
|
|
1069
|
+
return undefined;
|
|
1070
|
+
}
|
|
822
1071
|
/**
|
|
823
|
-
*
|
|
824
|
-
*
|
|
1072
|
+
* Resolve the `vault.required` POLICY for `cwd`: project config first, the
|
|
1073
|
+
* org profile second, the registered default (`false`) last. This is the ONE
|
|
1074
|
+
* reader of the policy ladder (13-REVIEW CR-02): the load gate
|
|
1075
|
+
* (`resolveVaultRequiredFlag` in `src/vault.cts`) asks it after its own
|
|
1076
|
+
* `vault.enabled` check, and the `vault org-profile` report asks it for the
|
|
1077
|
+
* `effective` row — so the report and the gate cannot disagree about which
|
|
1078
|
+
* level won for the same inputs. Before CR-02 this function read only the
|
|
1079
|
+
* root `.planning/config.json` and counted only `=== true || === 'true'`,
|
|
1080
|
+
* while the gate read the workstream config first and trimmed, so a project
|
|
1081
|
+
* `" true"` or `"yes"` was reported as overriding a profile `true` that the
|
|
1082
|
+
* gate in fact enforced.
|
|
1083
|
+
*
|
|
1084
|
+
* PROJECT CONFIG OUTRANKS THE PROFILE (T-02-17), so the ordering below is
|
|
1085
|
+
* load-bearing. Level 1 reads RAW config through `resolveConfigKey` with
|
|
1086
|
+
* `config: {}` and `registry: {}`: the workstream config.json, then the root
|
|
1087
|
+
* config.json, each in either dotted form (nested first), with no schema
|
|
1088
|
+
* default. A project value of `false` is a real value and wins. A project
|
|
1089
|
+
* value that is PRESENT BUT UNPARSEABLE (`booleanPolicyValue` answers
|
|
1090
|
+
* `undefined`) does not consume the decision: the profile is consulted as if
|
|
1091
|
+
* the project had said nothing, because a value that expresses no policy must
|
|
1092
|
+
* not outrank one that does. It is returned as `ignoredConfigValue` so the
|
|
1093
|
+
* gate can say so on stderr; this function prints nothing.
|
|
825
1094
|
*
|
|
826
|
-
*
|
|
827
|
-
*
|
|
828
|
-
*
|
|
829
|
-
* profile — an explicit project value is the project's decision.
|
|
830
|
-
* 2. The org profile, already narrowed to "true" / "false" by the value guard.
|
|
831
|
-
* 3. The registered default, false.
|
|
1095
|
+
* This answers the POLICY, not whether a gate exists: on a project with
|
|
1096
|
+
* `vault.enabled` off no gate renders and `resolveVaultRequiredFlag` answers
|
|
1097
|
+
* `false` whatever this returns.
|
|
832
1098
|
*
|
|
833
|
-
* Never throws
|
|
1099
|
+
* Never throws — a failed config read falls through to the profile, and the
|
|
1100
|
+
* profile seam already degrades to a miss on any fault.
|
|
834
1101
|
*/
|
|
835
1102
|
function resolveVaultRequired(cwd, opts) {
|
|
1103
|
+
// Level 1 — project config, raw (ws then root, either dotted form).
|
|
1104
|
+
let ignoredConfigValue;
|
|
1105
|
+
let ignored = false;
|
|
836
1106
|
try {
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
1107
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
1108
|
+
const activation = require('./capability-activation.cjs');
|
|
1109
|
+
const fromConfig = activation.resolveConfigKey(exports.VAULT_REQUIRED_CONFIG_KEY, { config: {}, cwd, registry: {} });
|
|
1110
|
+
if (fromConfig.found) {
|
|
1111
|
+
const decided = booleanPolicyValue(fromConfig.value);
|
|
1112
|
+
if (decided !== undefined)
|
|
1113
|
+
return { required: decided, source: 'config', warnings: [] };
|
|
1114
|
+
ignored = true;
|
|
1115
|
+
ignoredConfigValue = fromConfig.value;
|
|
844
1116
|
}
|
|
845
|
-
return { required: false, source: 'default', valid: true, raw: undefined, warnings: profile.warnings };
|
|
846
1117
|
}
|
|
847
|
-
catch {
|
|
848
|
-
|
|
1118
|
+
catch { /* fall through to the profile */ }
|
|
1119
|
+
const withIgnored = (r) => (ignored ? { ...r, ignoredConfigValue } : r);
|
|
1120
|
+
// Level 2 — the org profile. Already narrowed to 'true'/'false' by
|
|
1121
|
+
// profileValueDisposition; booleanPolicyValue agrees with it by construction.
|
|
1122
|
+
const profile = readProfileValue(cwd, exports.VAULT_REQUIRED_CONFIG_KEY, opts);
|
|
1123
|
+
if (profile.value !== undefined) {
|
|
1124
|
+
const decided = booleanPolicyValue(profile.value);
|
|
1125
|
+
if (decided !== undefined)
|
|
1126
|
+
return withIgnored({ required: decided, source: 'profile', warnings: profile.warnings });
|
|
849
1127
|
}
|
|
1128
|
+
// Level 3 — the registered default from capabilities/vault/capability.json.
|
|
1129
|
+
return withIgnored({ required: false, source: 'default', warnings: profile.warnings });
|
|
850
1130
|
}
|
|
851
|
-
// ─── resolveAcceptedTracePredicates ──────────────────────────────────────────
|
|
852
1131
|
/**
|
|
853
|
-
*
|
|
854
|
-
*
|
|
855
|
-
*
|
|
856
|
-
*
|
|
857
|
-
*
|
|
1132
|
+
* Resolve `vault.mode` for `cwd`: project config, then the org profile, then
|
|
1133
|
+
* the registered default `practices` — the same three levels as
|
|
1134
|
+
* `resolveVaultRequired` above.
|
|
1135
|
+
*
|
|
1136
|
+
* Like `resolveVaultRequired` (the one ladder both the load gate and the
|
|
1137
|
+
* report ask, since 13-REVIEW CR-02), this resolver is the ONE reader of the
|
|
1138
|
+
* mode for every mode-aware surface: the `vault
|
|
1139
|
+
* org-profile` report (13-05), the gate, refusals and status field (13-06)
|
|
1140
|
+
* and the parked practices surfaces (13-07). No surface re-reads the key
|
|
1141
|
+
* from config itself; the negative-grep anti-drift test in
|
|
1142
|
+
* `tests/vault-mode.test.cjs` is what keeps a second reader from appearing.
|
|
1143
|
+
*
|
|
1144
|
+
* Level 1 reads RAW config (D-21): `resolveConfigKey` with `config: {}` and
|
|
1145
|
+
* `registry: {}` reads the workstream config.json and then the root
|
|
1146
|
+
* config.json, each in either dotted form (nested first), with NO schema
|
|
1147
|
+
* default and no federated fallback. The merged config from
|
|
1148
|
+
* `config-loader.cts` is deliberately not used:
|
|
1149
|
+
* `federated-config`'s `_typeMatches` substitutes the schema default for an
|
|
1150
|
+
* out-of-enum value, and its warning is suppressed at load
|
|
1151
|
+
* (`config-loader.cts`), so `"whatever"` would silently read as `practices`.
|
|
1152
|
+
* A project value that is present but not one of the two literals returns
|
|
1153
|
+
* `ok: false` with `vault_mode_invalid` and NEVER falls back (D-08): an
|
|
1154
|
+
* unknown mode must be refused, not treated as practices.
|
|
1155
|
+
*
|
|
1156
|
+
* Level 2 is `readProfileValue`; `profileValueDisposition` has already
|
|
1157
|
+
* narrowed the profile value to one of the two literals or dropped it with a
|
|
1158
|
+
* warning (D-09), so an invalid profile value lands at level 3.
|
|
1159
|
+
*
|
|
1160
|
+
* KNOWN GAP (D-25), recorded rather than hidden: `config-get` reads only the
|
|
1161
|
+
* planning dir's config.json, while level 1 here reads the workstream config
|
|
1162
|
+
* and then the root config. In flat mode the two agree (pinned by the parity
|
|
1163
|
+
* test in `tests/vault-mode.test.cjs`); on a partitioned project a root-only
|
|
1164
|
+
* `vault.mode` can read differently. Filed as a todo; the same gap pre-exists
|
|
1165
|
+
* for `vault.required`.
|
|
1166
|
+
*
|
|
1167
|
+
* Never throws: both seams already degrade to a miss on any fault.
|
|
858
1168
|
*/
|
|
859
|
-
function
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
1169
|
+
function resolveVaultMode(cwd, opts) {
|
|
1170
|
+
// Level 1 — project config, raw (ws then root, either dotted form).
|
|
1171
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
1172
|
+
const activation = require('./capability-activation.cjs');
|
|
1173
|
+
const fromConfig = activation.resolveConfigKey(VAULT_MODE_CONFIG_KEY, { config: {}, cwd, registry: {} });
|
|
1174
|
+
if (fromConfig.found) {
|
|
1175
|
+
if (isVaultMode(fromConfig.value)) {
|
|
1176
|
+
return { ok: true, mode: fromConfig.value.trim(), source: 'config', warnings: [] };
|
|
1177
|
+
}
|
|
1178
|
+
return { ok: false, source: 'config', reason: VAULT_MODE_REASON.INVALID, warnings: [] };
|
|
866
1179
|
}
|
|
867
|
-
|
|
868
|
-
|
|
1180
|
+
// Level 2 — the org profile, already narrowed by profileValueDisposition.
|
|
1181
|
+
const profile = readProfileValue(cwd, VAULT_MODE_CONFIG_KEY, opts);
|
|
1182
|
+
if (isVaultMode(profile.value)) {
|
|
1183
|
+
return { ok: true, mode: profile.value.trim(), source: 'profile', warnings: profile.warnings };
|
|
869
1184
|
}
|
|
1185
|
+
// Level 3 — the registered default from capabilities/vault/capability.json.
|
|
1186
|
+
return { ok: true, mode: DEFAULT_VAULT_MODE, source: 'default', warnings: profile.warnings };
|
|
870
1187
|
}
|