devflow-kit 2.5.0 → 3.0.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/CHANGELOG.md +73 -0
- package/README.md +44 -19
- package/dist/agents/git.md +13 -15
- package/dist/cli/commands/ambient.js +160 -145
- package/dist/cli/commands/capture.js +29 -55
- package/dist/cli/commands/compliance.js +32 -61
- package/dist/cli/commands/context.js +17 -32
- package/dist/cli/commands/debug.js +65 -26
- package/dist/cli/commands/flags.js +3 -3
- package/dist/cli/commands/hud.js +34 -10
- package/dist/cli/commands/init-seed.js +40 -4
- package/dist/cli/commands/init.js +249 -271
- package/dist/cli/commands/install-report.js +10 -15
- package/dist/cli/commands/knowledge/index.js +1 -1
- package/dist/cli/commands/knowledge/toggle.js +11 -3
- package/dist/cli/commands/learning.js +52 -37
- package/dist/cli/commands/legacy-hooks.js +11 -14
- package/dist/cli/commands/memory.js +67 -78
- package/dist/cli/commands/proxy.js +23 -41
- package/dist/cli/commands/security.js +5 -13
- package/dist/cli/commands/skills.js +21 -3
- package/dist/cli/commands/tracker.js +100 -228
- package/dist/cli/commands/uninstall.js +343 -138
- package/dist/commands/bug-analysis.md +38 -12
- package/dist/commands/code-review.md +70 -21
- package/dist/commands/debug.md +37 -7
- package/dist/commands/dynamic-build.md +66 -17
- package/dist/commands/dynamic-plan.md +19 -8
- package/dist/commands/dynamic-profile.md +24 -10
- package/dist/commands/dynamic-tickets.md +22 -11
- package/dist/commands/explore.md +37 -7
- package/dist/commands/implement.md +96 -32
- package/dist/commands/plan.md +62 -19
- package/dist/commands/release.md +2 -2
- package/dist/commands/research.md +34 -8
- package/dist/commands/resolve.md +65 -17
- package/dist/commands/self-review.md +45 -9
- package/dist/core/compliance-compose.js +27 -27
- package/dist/core/evidence-policy.js +240 -24
- package/dist/core/feature-config.js +94 -25
- package/dist/core/feature-switch.js +1 -1
- package/dist/core/flags.js +30 -2
- package/dist/core/fs-atomic.js +27 -0
- package/dist/core/hook-log-dirs.js +104 -0
- package/dist/core/learning-tuning-config.js +5 -3
- package/dist/core/ledger-root.js +102 -0
- package/dist/core/manifest.js +6 -4
- package/dist/core/mds-variants.js +34 -97
- package/dist/core/migrations.js +49 -23
- package/dist/core/plugins.js +5 -4
- package/dist/core/project-paths.js +0 -17
- package/dist/core/same-location.js +25 -0
- package/dist/core/tracker.js +226 -139
- package/dist/hud/components/config-counts.js +15 -4
- package/dist/hud/components/learning-counts.js +14 -0
- package/dist/hud/config.js +2 -1
- package/dist/hud/cost-history.js +2 -4
- package/dist/hud/git.js +52 -7
- package/dist/hud/index.js +7 -9
- package/dist/skills/git/references/pr/check-merge-readiness.md +1 -1
- package/dist/skills/git/references/pr/ensure-pr-ready.md +1 -1
- package/dist/skills/git/references/pr/update-pr-evidence.md +1 -1
- package/dist/skills/git/references/tracker/_mcp.md +1 -1
- package/dist/skills/git/references/tracker/github/backlink-shipped-issues.md +1 -1
- package/dist/skills/git/references/tracker/github/ensure-traceable-issue.md +1 -1
- package/dist/skills/git/references/tracker/github/gather-release-evidence.md +2 -2
- package/dist/skills/git/references/tracker/github/manage-debt.md +3 -3
- package/dist/skills/git/references/tracker/jira/backlink-shipped-issues.md +1 -1
- package/dist/skills/git/references/tracker/jira/ensure-traceable-issue.md +1 -1
- package/dist/skills/git/references/tracker/jira/gather-release-evidence.md +2 -2
- package/dist/skills/git/references/tracker/jira/manage-debt.md +1 -1
- package/dist/skills/git/references/tracker/jira/post-wave-report.md +1 -1
- package/dist/skills/git/references/tracker/jira/setup-task.md +1 -1
- package/dist/skills/git/references/tracker/linear/backlink-shipped-issues.md +1 -1
- package/dist/skills/git/references/tracker/linear/ensure-traceable-issue.md +1 -1
- package/dist/skills/git/references/tracker/linear/gather-release-evidence.md +2 -2
- package/dist/skills/git/references/tracker/linear/manage-debt.md +1 -1
- package/dist/skills/git/references/tracker/linear/post-wave-report.md +1 -1
- package/dist/skills/git/references/tracker/linear/setup-task.md +1 -1
- package/dist/targets/claude-code/claude-paths.js +59 -57
- package/dist/targets/claude-code/compliance-install.js +49 -65
- package/dist/targets/claude-code/hooks.js +108 -3
- package/dist/targets/claude-code/installer.js +30 -57
- package/dist/targets/claude-code/post-install.js +232 -139
- package/dist/targets/claude-code/tracker-install.js +38 -65
- package/package.json +5 -4
- package/src/assets/agents/code.md +4 -3
- package/src/assets/agents/design.md +1 -0
- package/src/assets/agents/git.mds +55 -57
- package/src/assets/agents/knowledge.md +2 -2
- package/src/assets/agents/review.md +3 -1
- package/src/assets/agents/tracker.md +37 -30
- package/src/assets/commands/_partials/_compliance.mds +19 -1
- package/src/assets/commands/_partials/_decisions.mds +15 -3
- package/src/assets/commands/_partials/_docs_root.mds +35 -0
- package/src/assets/commands/_partials/_engine.mds +2 -2
- package/src/assets/commands/_partials/_evidence_policy.mds +3 -3
- package/src/assets/commands/_partials/_factory.mds +1 -1
- package/src/assets/commands/_partials/_knowledge.mds +27 -9
- package/src/assets/commands/_partials/_plan_contract.mds +2 -2
- package/src/assets/commands/_partials/_preamble.mds +1 -1
- package/src/assets/commands/_partials/_publication.mds +6 -2
- package/src/assets/commands/_partials/_settings.mds +28 -0
- package/src/assets/commands/_partials/_ticket_template.mds +3 -3
- package/src/assets/commands/_partials/_tracker.mds +4 -4
- package/src/assets/commands/_partials/_wave.mds +4 -4
- package/src/assets/commands/bug-analysis.mds +19 -17
- package/src/assets/commands/code-review.mds +39 -33
- package/src/assets/commands/debug.mds +4 -5
- package/src/assets/commands/dynamic-build.mds +75 -53
- package/src/assets/commands/dynamic-plan.mds +20 -15
- package/src/assets/commands/dynamic-profile.mds +24 -11
- package/src/assets/commands/dynamic-tickets.mds +25 -20
- package/src/assets/commands/explore.mds +4 -5
- package/src/assets/commands/implement.mds +58 -45
- package/src/assets/commands/plan.mds +34 -29
- package/src/assets/commands/release.md +2 -2
- package/src/assets/commands/research.mds +11 -9
- package/src/assets/commands/resolve.mds +41 -39
- package/src/assets/commands/self-review.mds +24 -25
- package/src/assets/mds/git/_pr.mds +61 -61
- package/src/assets/mds/git/_references.mds +19 -19
- package/src/assets/mds/tracker/_common.mds +8 -8
- package/src/assets/mds/tracker/_github.mds +71 -71
- package/src/assets/mds/tracker/_jira.mds +74 -74
- package/src/assets/mds/tracker/_linear.mds +75 -75
- package/src/assets/mds/tracker/_mcp.mds +23 -17
- package/src/assets/scripts/hooks/background-memory-update +35 -19
- package/src/assets/scripts/hooks/capture-prompt +18 -12
- package/src/assets/scripts/hooks/capture-question +18 -12
- package/src/assets/scripts/hooks/capture-turn +27 -17
- package/src/assets/scripts/hooks/debug-trace +11 -6
- package/src/assets/scripts/hooks/ensure-devflow-init +33 -6
- package/src/assets/scripts/hooks/ensure-proxy +9 -8
- package/src/assets/scripts/hooks/ensure-root-gitignore +111 -36
- package/src/assets/scripts/hooks/git-marker +48 -0
- package/src/assets/scripts/hooks/json-helper.cjs +6 -1
- package/src/assets/scripts/hooks/lib/project-paths.cjs +0 -19
- package/src/assets/scripts/hooks/log-paths +80 -0
- package/src/assets/scripts/hooks/memory-worker +17 -15
- package/src/assets/scripts/hooks/pre-compact-memory +41 -16
- package/src/assets/scripts/hooks/queue-append +104 -30
- package/src/assets/scripts/hooks/resolve-project-root +101 -7
- package/src/assets/scripts/hooks/session-start-context +289 -122
- package/src/assets/scripts/hooks/session-start-memory +35 -16
- package/src/assets/scripts/lib/project-config.cjs +633 -0
- package/src/assets/scripts/resolve-evidence-policy.cjs +300 -220
- package/src/assets/scripts/resolve-settings.cjs +1054 -0
- package/src/assets/scripts/verify-evidence.cjs +1 -1
- package/src/assets/skills/compliance/SKILL.md +2 -2
- package/src/assets/skills/docs-framework/SKILL.md +6 -7
- package/src/assets/skills/docs-framework/references/patterns.md +10 -17
- package/src/assets/skills/gap-analysis/SKILL.md +2 -2
- package/src/assets/skills/git/references/github-api.md +9 -9
- package/src/assets/skills/git/references/patterns.md +1 -1
- package/src/assets/skills/worktree-support/SKILL.md +1 -1
- package/src/assets/skills/worktree-support/references/roots.md +29 -0
- package/src/targets/claude-code/templates/managed-settings.json +25 -9
|
@@ -3,14 +3,17 @@
|
|
|
3
3
|
//
|
|
4
4
|
// Resolves EVIDENCE_POLICY for a repository and prints it on ONE line together
|
|
5
5
|
// with the three mechanism inputs that operations receive. Installed as a
|
|
6
|
-
// top-level sibling of hud.sh and redact-secrets.cjs under ~/.devflow/scripts
|
|
6
|
+
// top-level sibling of hud.sh and redact-secrets.cjs under ~/.devflow/scripts/,
|
|
7
|
+
// with its parser beside it in lib/project-config.cjs.
|
|
7
8
|
//
|
|
8
9
|
// Usage: node resolve-evidence-policy.cjs [<dir>] (<dir> defaults to cwd)
|
|
9
10
|
//
|
|
10
11
|
// The policy is plumbing, decided once by the caller: this script prints it plus
|
|
11
12
|
// the mechanism inputs, and operations only ever see the inputs. It WRITES NOTHING
|
|
12
|
-
// — no file, no git ref, no remote state — so `.devflow/
|
|
13
|
-
// team-owned file that only the team commits.
|
|
13
|
+
// — no file, no git ref, no remote state — so `.devflow/project.json` stays a
|
|
14
|
+
// team-owned file that only the team commits (applies ADR-024). A committed
|
|
15
|
+
// `.devflow/policy.json` is detected by presence and never parsed
|
|
16
|
+
// (D-POLICY-JSON-RETIRED).
|
|
14
17
|
//
|
|
15
18
|
// stdout is exactly one line plus "\n", or empty (D-POLICY-LINE):
|
|
16
19
|
// EVIDENCE_POLICY=<required|standard> SOURCE=<file|worktree|default|invalid|error>
|
|
@@ -38,8 +41,8 @@
|
|
|
38
41
|
// exitCode assignment, so nothing is truncated and no cleanup is skipped
|
|
39
42
|
// - every subprocess is spawned with an argv array (never a shell), stdin
|
|
40
43
|
// ignored, a timeout and a maxBuffer; every loop has a fixed bound
|
|
41
|
-
// - a
|
|
42
|
-
//
|
|
44
|
+
// - a project.json that is not a regular file is never opened, and one over
|
|
45
|
+
// MAX_CONFIG_BYTES is never read; a working-tree policy.json is never opened
|
|
43
46
|
// - no index-refreshing git command runs (no `status`, no `diff`), so a
|
|
44
47
|
// repository's configured fsmonitor hook never fires; no fetch, no set-head
|
|
45
48
|
|
|
@@ -47,8 +50,11 @@
|
|
|
47
50
|
|
|
48
51
|
const fs = require('fs');
|
|
49
52
|
const path = require('path');
|
|
50
|
-
const os = require('os');
|
|
51
53
|
const childProcess = require('child_process');
|
|
54
|
+
const projectConfig = require('./lib/project-config.cjs');
|
|
55
|
+
|
|
56
|
+
/** Largest project.json read, in bytes — the shared parser's bound. */
|
|
57
|
+
const { MAX_CONFIG_BYTES } = projectConfig;
|
|
52
58
|
|
|
53
59
|
// ---------------------------------------------------------------------------
|
|
54
60
|
// Closed vocabularies
|
|
@@ -67,7 +73,8 @@ const SOURCES = Object.freeze(/** @type {Source[]} */ (['file', 'worktree', 'def
|
|
|
67
73
|
/**
|
|
68
74
|
* Every WARN token, in the order the line prints them.
|
|
69
75
|
* remote-unavailable the default branch's file could not be consulted
|
|
70
|
-
* invalid-file a folded
|
|
76
|
+
* invalid-file a folded source (remote, worktree or tracking copy) was invalid —
|
|
77
|
+
* its project.json evidence, or the retired policy.json's presence
|
|
71
78
|
* raised-by-compliance compliance raised a `standard` file or worktree policy
|
|
72
79
|
* pr-changes-policy HEAD or the worktree differs from the default branch (advisory)
|
|
73
80
|
*/
|
|
@@ -116,19 +123,11 @@ const MECHANISM_INPUTS = Object.freeze({
|
|
|
116
123
|
/** The mechanism-input keys, in line order. */
|
|
117
124
|
const INPUT_KEYS = Object.freeze(['ISSUE_REQUIRED', 'APPLY_CONVENTIONS', 'REQUIRE_NON_AUTHOR_APPROVAL']);
|
|
118
125
|
|
|
119
|
-
/** Largest policy file read, in bytes. A policy is two keys; nothing valid approaches this. */
|
|
120
|
-
const MAX_POLICY_BYTES = 4096;
|
|
121
|
-
|
|
122
|
-
/** Largest manifest read, in bytes. */
|
|
123
|
-
const MAX_MANIFEST_BYTES = 1048576;
|
|
124
|
-
|
|
125
126
|
/**
|
|
126
|
-
* A branch name this script will put into argv or onto stdout
|
|
127
|
-
*
|
|
128
|
-
* of `[A-Za-z0-9._/-]`, and never `..`. The lookahead is bounded by the same
|
|
129
|
-
* class, so the scan is linear (no unbounded `.*`).
|
|
127
|
+
* A branch name this script will put into argv or onto stdout — the shared gate
|
|
128
|
+
* (lib/project-config.cjs), so the settings resolver reads the same branch names.
|
|
130
129
|
*/
|
|
131
|
-
const SAFE_REF_RE =
|
|
130
|
+
const { SAFE_REF_RE } = projectConfig;
|
|
132
131
|
|
|
133
132
|
/**
|
|
134
133
|
* D-POLICY-LINE: the exported output grammar — anchored, closed alternations
|
|
@@ -153,14 +152,17 @@ const FAIL_CLOSED_LINE =
|
|
|
153
152
|
const GH_TIMEOUT_MS = 10000;
|
|
154
153
|
const GIT_REMOTE_TIMEOUT_MS = 10000;
|
|
155
154
|
const GIT_LOCAL_TIMEOUT_MS = 5000;
|
|
156
|
-
/** gh answers and blob reads: generous for a 4 KiB
|
|
155
|
+
/** gh answers and blob reads: generous for a 4 KiB project.json, and an overflow is ENOBUFS ⇒ invalid. */
|
|
157
156
|
const BLOB_MAX_BUFFER = 65536;
|
|
158
157
|
/** rev-parse and ls-remote print one short line each. */
|
|
159
158
|
const LINE_MAX_BUFFER = 4096;
|
|
160
159
|
|
|
161
|
-
/** The repository-relative path of the policy file, in git and API spelling. */
|
|
160
|
+
/** The repository-relative path of the retired policy file, in git and API spelling (D-POLICY-JSON-RETIRED). */
|
|
162
161
|
const POLICY_REL = '.devflow/policy.json';
|
|
163
162
|
|
|
163
|
+
/** The repository-relative path of the team config file, in git and API spelling. */
|
|
164
|
+
const PROJECT_REL = '.devflow/project.json';
|
|
165
|
+
|
|
164
166
|
/** `git ls-remote --symref origin HEAD`, first line. Bounded class, no backtracking. */
|
|
165
167
|
const LS_REMOTE_SYMREF_RE = /^ref: refs\/heads\/([^\t\n]{1,255})\tHEAD$/;
|
|
166
168
|
|
|
@@ -241,76 +243,6 @@ function parseArgs(argv) {
|
|
|
241
243
|
return { kind: 'resolve', dir: positionals.length === 1 ? positionals[0] : process.cwd() };
|
|
242
244
|
}
|
|
243
245
|
|
|
244
|
-
// ---------------------------------------------------------------------------
|
|
245
|
-
// parsePolicyBytes (D-POLICY-STRICT-SCHEMA)
|
|
246
|
-
// ---------------------------------------------------------------------------
|
|
247
|
-
|
|
248
|
-
/**
|
|
249
|
-
* D-POLICY-STRICT-SCHEMA: the grammar gate a policy file must pass BEFORE
|
|
250
|
-
* JSON.parse sees it. Exactly two members, each key `version` or
|
|
251
|
-
* `evidencePolicy`, each value `1`, `"required"` or `"standard"`, and JSON
|
|
252
|
-
* whitespace only — `[ \t\r\n]`, never `\s`, which admits U+FEFF and U+00A0.
|
|
253
|
-
* Each `*` run is separated by a literal, so matching is linear. What it keeps
|
|
254
|
-
* out never reaches the parser: `__proto__`/`constructor` and every other key,
|
|
255
|
-
* `\u`-escaped keys or values, `1.0`, `"1"`, arrays, `null`, trailing bytes.
|
|
256
|
-
*/
|
|
257
|
-
const POLICY_GRAMMAR_RE =
|
|
258
|
-
/^[ \t\r\n]*\{[ \t\r\n]*"(?:version|evidencePolicy)"[ \t\r\n]*:[ \t\r\n]*(?:1|"required"|"standard")[ \t\r\n]*,[ \t\r\n]*"(?:version|evidencePolicy)"[ \t\r\n]*:[ \t\r\n]*(?:1|"required"|"standard")[ \t\r\n]*\}[ \t\r\n]*$/;
|
|
259
|
-
|
|
260
|
-
/**
|
|
261
|
-
* Classify policy-file bytes. `null`/`undefined` is "no file" (absent); everything
|
|
262
|
-
* else is valid or invalid, never absent — an empty committed file is invalid.
|
|
263
|
-
*
|
|
264
|
-
* The byte checks run before decoding (size, then a UTF-8 BOM, which is invalid
|
|
265
|
-
* rather than skipped), decoding is fatal on a malformed sequence, and after the
|
|
266
|
-
* grammar gate the parsed object must hold exactly the own keys
|
|
267
|
-
* {version, evidencePolicy} with version === 1. A duplicate key passes the
|
|
268
|
-
* grammar but collapses in the parse, so the key-set check refuses it.
|
|
269
|
-
*
|
|
270
|
-
* @param {Uint8Array | null | undefined} buf
|
|
271
|
-
* @returns {ParsedPolicy}
|
|
272
|
-
*/
|
|
273
|
-
function parsePolicyBytes(buf) {
|
|
274
|
-
if (buf === null || buf === undefined) return ABSENT;
|
|
275
|
-
if (!(buf instanceof Uint8Array)) return INVALID;
|
|
276
|
-
if (buf.length > MAX_POLICY_BYTES) return INVALID;
|
|
277
|
-
if (buf.length >= 3 && buf[0] === 0xef && buf[1] === 0xbb && buf[2] === 0xbf) return INVALID;
|
|
278
|
-
|
|
279
|
-
let text;
|
|
280
|
-
try {
|
|
281
|
-
// TextDecoder would silently strip a leading BOM; the byte check above is
|
|
282
|
-
// what makes one invalid.
|
|
283
|
-
text = new TextDecoder('utf-8', { fatal: true }).decode(buf);
|
|
284
|
-
} catch (_) {
|
|
285
|
-
return INVALID;
|
|
286
|
-
}
|
|
287
|
-
if (!POLICY_GRAMMAR_RE.test(text)) return INVALID;
|
|
288
|
-
|
|
289
|
-
let parsed;
|
|
290
|
-
try {
|
|
291
|
-
parsed = JSON.parse(text);
|
|
292
|
-
} catch (_) {
|
|
293
|
-
return INVALID;
|
|
294
|
-
}
|
|
295
|
-
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) return INVALID;
|
|
296
|
-
const keys = Object.keys(parsed);
|
|
297
|
-
if (keys.length !== 2 || !keys.includes('version') || !keys.includes('evidencePolicy')) return INVALID;
|
|
298
|
-
if (parsed.version !== 1) return INVALID;
|
|
299
|
-
if (!POLICIES.includes(parsed.evidencePolicy)) return INVALID;
|
|
300
|
-
return Object.freeze({ kind: 'valid', policy: parsed.evidencePolicy });
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
/**
|
|
304
|
-
* The canonical bytes of a policy file — what the CLI suggests a team commit.
|
|
305
|
-
*
|
|
306
|
-
* @param {unknown} policy
|
|
307
|
-
* @returns {string | null} `{"version":1,"evidencePolicy":"<p>"}\n`, or null outside POLICIES
|
|
308
|
-
*/
|
|
309
|
-
function serializePolicy(policy) {
|
|
310
|
-
if (!POLICIES.includes(/** @type {Policy} */ (policy))) return null;
|
|
311
|
-
return JSON.stringify({ version: 1, evidencePolicy: policy }) + '\n';
|
|
312
|
-
}
|
|
313
|
-
|
|
314
246
|
// ---------------------------------------------------------------------------
|
|
315
247
|
// Compliance default
|
|
316
248
|
// ---------------------------------------------------------------------------
|
|
@@ -340,117 +272,148 @@ function complianceDefault(rawFeatureValue) {
|
|
|
340
272
|
// Bounded file reads
|
|
341
273
|
// ---------------------------------------------------------------------------
|
|
342
274
|
|
|
275
|
+
/** Bounded, never-writing file read — the shared one (lib/project-config.cjs). */
|
|
276
|
+
const readBoundedRegularFile = projectConfig.readBoundedRegularFile;
|
|
277
|
+
|
|
343
278
|
/**
|
|
344
|
-
*
|
|
279
|
+
* D-POLICY-JSON-RETIRED: `.devflow/policy.json` is never parsed. At a source whose
|
|
280
|
+
* project.json does not decide the evidence (absent, or present without an
|
|
281
|
+
* `evidence` key), an existing policy.json makes that source `invalid` — whatever
|
|
282
|
+
* its bytes say, `standard` and unreadable alike — so the repository resolves
|
|
283
|
+
* `required` with `invalid-file` until the team states its value as `evidence` in
|
|
284
|
+
* project.json. With neither file the source is absent. Presence is the whole
|
|
285
|
+
* signal, and each source probes it with the call it already makes:
|
|
286
|
+
* R the default branch's contents call — exit 0 or an overflow is present, a
|
|
287
|
+
* 404 absent, anything else the remote unavailable (remoteSource)
|
|
288
|
+
* T/H `git cat-file blob` — exit 0 present, an answered non-zero exit absent,
|
|
289
|
+
* an unanswered call invalid (catFilePolicyPresence)
|
|
290
|
+
* W an lstat that never opens the file — ENOENT or ENOTDIR absent, anything
|
|
291
|
+
* else present, a symlink or FIFO included (worktreePolicyPresence)
|
|
292
|
+
* The bytes a call returns are discarded unread, so no value in the file can
|
|
293
|
+
* lower the policy or reach stdout.
|
|
345
294
|
*
|
|
346
|
-
*
|
|
347
|
-
*
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
295
|
+
* @param {boolean} present
|
|
296
|
+
* @returns {ParsedPolicy}
|
|
297
|
+
*/
|
|
298
|
+
function retiredPolicyReading(present) {
|
|
299
|
+
return present ? INVALID : ABSENT;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* W — whether the working tree holds a policy.json. lstat only: the file is never
|
|
304
|
+
* opened, so a FIFO cannot block and a symlink is never followed. An lstat that
|
|
305
|
+
* fails for any reason but "no such path" cannot prove absence, so it reads as
|
|
306
|
+
* present (fail closed).
|
|
352
307
|
*
|
|
353
|
-
* @param {string}
|
|
354
|
-
* @
|
|
355
|
-
* @param {boolean} followSymlinks
|
|
356
|
-
* @returns {{ kind: 'absent' } | { kind: 'refused' } | { kind: 'ok', bytes: Buffer }}
|
|
308
|
+
* @param {string} root
|
|
309
|
+
* @returns {ParsedPolicy}
|
|
357
310
|
*/
|
|
358
|
-
function
|
|
359
|
-
let st;
|
|
311
|
+
function worktreePolicyPresence(root) {
|
|
360
312
|
try {
|
|
361
|
-
|
|
313
|
+
fs.lstatSync(path.join(root, '.devflow', 'policy.json'));
|
|
314
|
+
return retiredPolicyReading(true);
|
|
362
315
|
} catch (/** @type {any} */ err) {
|
|
363
|
-
return err && (err.code === 'ENOENT' || err.code === 'ENOTDIR')
|
|
316
|
+
return retiredPolicyReading(!(err && (err.code === 'ENOENT' || err.code === 'ENOTDIR')));
|
|
364
317
|
}
|
|
365
|
-
|
|
318
|
+
}
|
|
366
319
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
320
|
+
// ---------------------------------------------------------------------------
|
|
321
|
+
// Source readings (D-POLICY-SOURCE-PRECEDENCE, D-COMPLIANCE-REPO-FLOOR)
|
|
322
|
+
// ---------------------------------------------------------------------------
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* @typedef {{ evidence: ParsedPolicy, compliance: boolean }} SourceReading
|
|
326
|
+
* What one source (R, T, H or W) says: its evidence policy, and whether its
|
|
327
|
+
* project.json declares repository compliance.
|
|
328
|
+
*
|
|
329
|
+
* @typedef {{ evidence: ParsedPolicy | null, compliance: boolean }} ProjectReading
|
|
330
|
+
* `evidence` null ⇒ project.json does not decide it, so the presence of the
|
|
331
|
+
* retired policy file at the same source does (D-POLICY-JSON-RETIRED).
|
|
332
|
+
*
|
|
333
|
+
* @typedef {{ kind: 'absent' } | { kind: 'invalid' } | { kind: 'parsed', evidence: { kind: string, value?: unknown }, compliance: { kind: string } }} ProjectConfigView
|
|
334
|
+
* The part of lib/project-config.cjs ProjectConfig this script reads.
|
|
335
|
+
*/
|
|
336
|
+
|
|
337
|
+
/** @type {ProjectConfigView} */
|
|
338
|
+
const PROJECT_ABSENT = Object.freeze({ kind: 'absent' });
|
|
339
|
+
/** @type {ProjectConfigView} */
|
|
340
|
+
const PROJECT_INVALID = Object.freeze({ kind: 'invalid' });
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* D-POLICY-SOURCE-PRECEDENCE: at every source, the `evidence` key of
|
|
344
|
+
* `project.json` is the one authority on the evidence policy. A valid value
|
|
345
|
+
* decides it, and a present-but-malformed or duplicated `evidence` decides
|
|
346
|
+
* `invalid` (⇒ required); a project.json that is not a JSON object at all is
|
|
347
|
+
* invalid outright. Only where project.json is absent, or present without an
|
|
348
|
+
* `evidence` key, does the same source's retired `policy.json` count — by
|
|
349
|
+
* presence alone (D-POLICY-JSON-RETIRED). The precedence is per source, so each
|
|
350
|
+
* source says ONE thing and foldPolicy folds them unchanged.
|
|
351
|
+
*
|
|
352
|
+
* D-COMPLIANCE-REPO-FLOOR: a `compliance` key in project.json at R, T or W — any
|
|
353
|
+
* value, an empty list and a malformed one included — makes the repository's
|
|
354
|
+
* compliance default `required`, folded with the machine's as
|
|
355
|
+
* stricter(C_machine, C_repo). A machine with compliance on at zero frameworks is
|
|
356
|
+
* already `required`, and a repository that declares compliance is held to the
|
|
357
|
+
* same floor. A project.json that cannot be read counts as declaring it (fail
|
|
358
|
+
* closed).
|
|
359
|
+
*
|
|
360
|
+
* @param {ProjectConfigView} project
|
|
361
|
+
* @returns {ProjectReading}
|
|
362
|
+
*/
|
|
363
|
+
function projectReading(project) {
|
|
364
|
+
if (project.kind === 'absent') return { evidence: null, compliance: false };
|
|
365
|
+
if (project.kind === 'invalid') return { evidence: INVALID, compliance: true };
|
|
366
|
+
const compliance = project.compliance.kind !== 'absent';
|
|
367
|
+
const evidence = project.evidence;
|
|
368
|
+
if (evidence.kind === 'valid' && POLICIES.includes(/** @type {Policy} */ (evidence.value))) {
|
|
369
|
+
return { evidence: Object.freeze({ kind: 'valid', policy: /** @type {Policy} */ (evidence.value) }), compliance };
|
|
395
370
|
}
|
|
371
|
+
if (evidence.kind === 'absent') return { evidence: null, compliance };
|
|
372
|
+
return { evidence: INVALID, compliance };
|
|
396
373
|
}
|
|
397
374
|
|
|
398
375
|
/**
|
|
399
|
-
*
|
|
400
|
-
*
|
|
401
|
-
* unread.
|
|
376
|
+
* Complete a project reading with the retired policy file's presence when
|
|
377
|
+
* project.json did not decide the evidence (D-POLICY-JSON-RETIRED).
|
|
402
378
|
*
|
|
403
|
-
* @param {
|
|
404
|
-
* @
|
|
379
|
+
* @param {ProjectReading} reading
|
|
380
|
+
* @param {() => ParsedPolicy} probePolicy called only when needed
|
|
381
|
+
* @returns {SourceReading}
|
|
405
382
|
*/
|
|
406
|
-
function
|
|
407
|
-
|
|
408
|
-
if (read.kind === 'absent') return ABSENT;
|
|
409
|
-
if (read.kind === 'refused') return INVALID;
|
|
410
|
-
return parsePolicyBytes(read.bytes);
|
|
383
|
+
function withRetiredPolicy(reading, probePolicy) {
|
|
384
|
+
return { evidence: reading.evidence === null ? probePolicy() : reading.evidence, compliance: reading.compliance };
|
|
411
385
|
}
|
|
412
386
|
|
|
413
387
|
/**
|
|
414
|
-
*
|
|
415
|
-
*
|
|
388
|
+
* W — the working tree's project.json, then (only when it has no `evidence`)
|
|
389
|
+
* whether a policy.json is there. project.json is lstat-refused, never followed:
|
|
390
|
+
* a symlink, directory, FIFO or device is invalid unopened, and an oversize file
|
|
391
|
+
* invalid unread. policy.json is never opened at all.
|
|
416
392
|
*
|
|
417
|
-
* @
|
|
393
|
+
* @param {string} root
|
|
394
|
+
* @returns {SourceReading}
|
|
418
395
|
*/
|
|
419
|
-
function
|
|
420
|
-
const
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
home = os.homedir();
|
|
425
|
-
} catch (_) {
|
|
426
|
-
// No HOME and no passwd entry: there is no manifest to read, which is not an error.
|
|
427
|
-
return null;
|
|
428
|
-
}
|
|
429
|
-
return typeof home === 'string' && path.isAbsolute(home) ? path.join(home, '.devflow') : null;
|
|
396
|
+
function readWorktreeSource(root) {
|
|
397
|
+
const read = readBoundedRegularFile(path.join(root, '.devflow', 'project.json'), MAX_CONFIG_BYTES, false);
|
|
398
|
+
const project = read.kind === 'ok' ? projectConfig.parseProjectBytes(read.bytes)
|
|
399
|
+
: read.kind === 'absent' ? PROJECT_ABSENT : PROJECT_INVALID;
|
|
400
|
+
return withRetiredPolicy(projectReading(project), () => worktreePolicyPresence(root));
|
|
430
401
|
}
|
|
431
402
|
|
|
432
403
|
/**
|
|
433
|
-
* The raw `features.compliance` value from the manifest, or undefined.
|
|
434
|
-
*
|
|
435
|
-
*
|
|
436
|
-
*
|
|
437
|
-
*
|
|
404
|
+
* The raw `features.compliance` value from the machine manifest, or undefined.
|
|
405
|
+
* The manifest is read by the shared reader (lib/project-config.cjs
|
|
406
|
+
* readMachineManifest) — never through the CLI's manifest reader, which
|
|
407
|
+
* heal-writes. Anything unreadable or malformed is "no state", which
|
|
408
|
+
* complianceDefault maps to disabled; since C only ever raises, a missing C can
|
|
409
|
+
* never lower the result below the governing file.
|
|
438
410
|
*
|
|
439
411
|
* @returns {unknown}
|
|
440
412
|
*/
|
|
441
413
|
function readManifestCompliance() {
|
|
442
|
-
const
|
|
443
|
-
if (dir === null) return undefined;
|
|
444
|
-
const read = readBoundedRegularFile(path.join(dir, 'manifest.json'), MAX_MANIFEST_BYTES, true);
|
|
445
|
-
if (read.kind !== 'ok') return undefined;
|
|
446
|
-
let parsed;
|
|
447
|
-
try {
|
|
448
|
-
parsed = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(read.bytes));
|
|
449
|
-
} catch (_) {
|
|
450
|
-
return undefined;
|
|
451
|
-
}
|
|
414
|
+
const parsed = projectConfig.readMachineManifest();
|
|
452
415
|
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) return undefined;
|
|
453
|
-
const features = parsed.features;
|
|
416
|
+
const features = /** @type {any} */ (parsed).features;
|
|
454
417
|
if (features === null || typeof features !== 'object' || Array.isArray(features)) return undefined;
|
|
455
418
|
return Object.prototype.hasOwnProperty.call(features, 'compliance') ? features.compliance : undefined;
|
|
456
419
|
}
|
|
@@ -565,15 +528,20 @@ function probeDefaultBranch(ctx, root) {
|
|
|
565
528
|
}
|
|
566
529
|
|
|
567
530
|
/**
|
|
568
|
-
*
|
|
569
|
-
|
|
531
|
+
* @typedef {{ kind: 'bytes', bytes: Buffer } | { kind: 'absent' } | { kind: 'overflow' }} RemoteFile
|
|
532
|
+
*/
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Step 3: one file on the default branch — or null when the remote turned out to
|
|
536
|
+
* be unavailable after all.
|
|
570
537
|
*
|
|
571
538
|
* `--method GET` is mandatory: gh sends POST whenever a field is added. stdout is
|
|
572
539
|
* file content ONLY on exit 0 — a 404 prints gh's JSON error body on stdout, so
|
|
573
540
|
* it is classified from stderr and the exit code and its stdout is never parsed.
|
|
574
|
-
* ENOBUFS ⇒ invalid
|
|
575
|
-
* file would hand control to
|
|
576
|
-
*
|
|
541
|
+
* ENOBUFS ⇒ overflow — invalid, never unavailable,
|
|
542
|
+
* or a huge file would hand control to
|
|
543
|
+
* the worktree
|
|
544
|
+
* exit 0 ⇒ the bytes
|
|
577
545
|
* exit ≠ 0 and stderr has `(HTTP 404)` ⇒ absent — the probe already succeeded,
|
|
578
546
|
* so 404 means "no file", not "no access"
|
|
579
547
|
* anything else (403/429, 5xx, timeout, spawn error) ⇒ unavailable
|
|
@@ -581,19 +549,42 @@ function probeDefaultBranch(ctx, root) {
|
|
|
581
549
|
* @param {CallContext} ctx
|
|
582
550
|
* @param {string} root
|
|
583
551
|
* @param {string} ref
|
|
584
|
-
* @
|
|
552
|
+
* @param {string} rel PROJECT_REL or POLICY_REL — a constant, never input
|
|
553
|
+
* @returns {RemoteFile | null}
|
|
585
554
|
*/
|
|
586
|
-
function
|
|
555
|
+
function fetchRemoteFile(ctx, root, ref, rel) {
|
|
587
556
|
const r = runCall(ctx, 'gh', [
|
|
588
|
-
'api', '--method', 'GET', 'repos/{owner}/{repo}/contents/' +
|
|
557
|
+
'api', '--method', 'GET', 'repos/{owner}/{repo}/contents/' + rel,
|
|
589
558
|
'-f', 'ref=' + ref, '-H', 'Accept: application/vnd.github.raw+json',
|
|
590
559
|
], root, GH_TIMEOUT_MS, BLOB_MAX_BUFFER);
|
|
591
|
-
if (r.errorCode === 'ENOBUFS') return
|
|
592
|
-
if (r.ok) return
|
|
593
|
-
if (r.errorCode === null && r.status !== null && r.status !== 0 && r.stderr.includes('(HTTP 404)')) return
|
|
560
|
+
if (r.errorCode === 'ENOBUFS') return { kind: 'overflow' };
|
|
561
|
+
if (r.ok) return { kind: 'bytes', bytes: r.stdout };
|
|
562
|
+
if (r.errorCode === null && r.status !== null && r.status !== 0 && r.stderr.includes('(HTTP 404)')) return { kind: 'absent' };
|
|
594
563
|
return null;
|
|
595
564
|
}
|
|
596
565
|
|
|
566
|
+
/**
|
|
567
|
+
* R — the default branch's project.json, then (only when it is a 404 or has no
|
|
568
|
+
* `evidence`) whether its policy.json exists — the contents call's bytes are
|
|
569
|
+
* discarded unread (D-POLICY-JSON-RETIRED). Null when either call finds the
|
|
570
|
+
* remote unavailable: a half-read default branch is not a default branch.
|
|
571
|
+
*
|
|
572
|
+
* @param {CallContext} ctx
|
|
573
|
+
* @param {string} root
|
|
574
|
+
* @param {string} ref
|
|
575
|
+
* @returns {SourceReading | null}
|
|
576
|
+
*/
|
|
577
|
+
function remoteSource(ctx, root, ref) {
|
|
578
|
+
const projectFile = fetchRemoteFile(ctx, root, ref, PROJECT_REL);
|
|
579
|
+
if (projectFile === null) return null;
|
|
580
|
+
const reading = projectReading(projectFile.kind === 'bytes' ? projectConfig.parseProjectBytes(projectFile.bytes)
|
|
581
|
+
: projectFile.kind === 'absent' ? PROJECT_ABSENT : PROJECT_INVALID);
|
|
582
|
+
if (reading.evidence !== null) return { evidence: reading.evidence, compliance: reading.compliance };
|
|
583
|
+
const policyFile = fetchRemoteFile(ctx, root, ref, POLICY_REL);
|
|
584
|
+
if (policyFile === null) return null;
|
|
585
|
+
return withRetiredPolicy(reading, () => retiredPolicyReading(policyFile.kind !== 'absent'));
|
|
586
|
+
}
|
|
587
|
+
|
|
597
588
|
/**
|
|
598
589
|
* Step 5 (offline, D unknown): the default branch from `ls-remote --symref`.
|
|
599
590
|
* Only line 1 is read, and its capture must still pass SAFE_REF_RE.
|
|
@@ -612,27 +603,94 @@ function lsRemoteDefaultBranch(ctx, root) {
|
|
|
612
603
|
}
|
|
613
604
|
|
|
614
605
|
/**
|
|
615
|
-
*
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
*
|
|
606
|
+
* @typedef {{ kind: 'ref', ref: string } | { kind: 'none' } | { kind: 'unreadable' }} OriginHead
|
|
607
|
+
*/
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Step 5b (offline, D still unknown after ls-remote) — D-OFFLINE-ORIGIN-HEAD:
|
|
611
|
+
* the default branch this clone last recorded for origin, read LOCALLY with
|
|
612
|
+
* `git symbolic-ref --quiet refs/remotes/origin/HEAD` (no network, no index
|
|
613
|
+
* refresh). With D unnamed an unreachable origin leaves the worktree's own file
|
|
614
|
+
* governing, so a branch that sets `evidence:"standard"` would resolve standard
|
|
615
|
+
* while main says required; naming D here lets trackingSource fold main's
|
|
616
|
+
* tracking copy in, so the branch cannot lower it.
|
|
617
|
+
* ref exit 0 with one `refs/remotes/origin/<SAFE_REF_RE>` line
|
|
618
|
+
* none an answered non-zero exit: origin/HEAD is not recorded — the
|
|
619
|
+
* residual case, where the worktree file governs with
|
|
620
|
+
* WARN=remote-unavailable
|
|
621
|
+
* unreadable git did not answer, or origin/HEAD exists but its answer names
|
|
622
|
+
* no safe branch (another remote, a hostile or unparseable name).
|
|
623
|
+
* Either is a failure, never the residual case: gatherFacts reads
|
|
624
|
+
* it as an invalid base, which resolves required (avoids PF-075).
|
|
625
|
+
* resolve-settings' defaultBranchCompliance fails the same answer
|
|
626
|
+
* closed, to `generic`.
|
|
627
|
+
*
|
|
628
|
+
* @param {CallContext} ctx
|
|
629
|
+
* @param {string} root
|
|
630
|
+
* @returns {OriginHead}
|
|
631
|
+
*/
|
|
632
|
+
function localOriginHead(ctx, root) {
|
|
633
|
+
const r = runCall(ctx, 'git', ['symbolic-ref', '--quiet', 'refs/remotes/origin/HEAD'], root,
|
|
634
|
+
GIT_LOCAL_TIMEOUT_MS, LINE_MAX_BUFFER);
|
|
635
|
+
if (!answered(r)) return { kind: 'unreadable' };
|
|
636
|
+
if (!r.ok) return { kind: 'none' };
|
|
637
|
+
const ref = projectConfig.parseOriginHeadRef(r.stdout.toString('utf8'));
|
|
638
|
+
return ref === null ? { kind: 'unreadable' } : { kind: 'ref', ref };
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* Steps 4 and 7: one blob at a revision (HEAD, or the tracking ref).
|
|
643
|
+
* Exit 0 ⇒ the bytes; an answered non-zero exit (the path does not exist there,
|
|
644
|
+
* an unborn HEAD) ⇒ absent; an unanswered call (ENOBUFS included) ⇒ unanswered,
|
|
645
|
+
* which every caller reads as invalid — never absent, or a git that timed out
|
|
646
|
+
* would silently drop the default branch's copy from the fold.
|
|
620
647
|
*
|
|
621
648
|
* @param {CallContext} ctx
|
|
622
649
|
* @param {string} root
|
|
623
650
|
* @param {string} revision
|
|
624
|
-
* @
|
|
651
|
+
* @param {string} rel PROJECT_REL or POLICY_REL — a constant, never input
|
|
652
|
+
* @returns {{ kind: 'bytes', bytes: Buffer } | { kind: 'absent' } | { kind: 'unanswered' }}
|
|
625
653
|
*/
|
|
626
|
-
function
|
|
627
|
-
const r = runCall(ctx, 'git', ['cat-file', 'blob', revision + ':' +
|
|
654
|
+
function catFileBlob(ctx, root, revision, rel) {
|
|
655
|
+
const r = runCall(ctx, 'git', ['cat-file', 'blob', revision + ':' + rel], root,
|
|
628
656
|
GIT_LOCAL_TIMEOUT_MS, BLOB_MAX_BUFFER);
|
|
629
|
-
if (r.ok) return
|
|
630
|
-
return answered(r) ?
|
|
657
|
+
if (r.ok) return { kind: 'bytes', bytes: r.stdout };
|
|
658
|
+
return answered(r) ? { kind: 'absent' } : { kind: 'unanswered' };
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* Whether a revision holds the retired policy blob (D-POLICY-JSON-RETIRED): its
|
|
663
|
+
* bytes are discarded unread, and an unanswered read cannot prove absence, so it
|
|
664
|
+
* reads as present.
|
|
665
|
+
*
|
|
666
|
+
* @param {CallContext} ctx
|
|
667
|
+
* @param {string} root
|
|
668
|
+
* @param {string} revision
|
|
669
|
+
* @returns {ParsedPolicy}
|
|
670
|
+
*/
|
|
671
|
+
function catFilePolicyPresence(ctx, root, revision) {
|
|
672
|
+
return retiredPolicyReading(catFileBlob(ctx, root, revision, POLICY_REL).kind !== 'absent');
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* T or H: a revision's project.json, then (only when it is absent or has no
|
|
677
|
+
* `evidence`) whether its policy.json exists.
|
|
678
|
+
*
|
|
679
|
+
* @param {CallContext} ctx
|
|
680
|
+
* @param {string} root
|
|
681
|
+
* @param {string} revision
|
|
682
|
+
* @returns {SourceReading}
|
|
683
|
+
*/
|
|
684
|
+
function revisionSource(ctx, root, revision) {
|
|
685
|
+
const blob = catFileBlob(ctx, root, revision, PROJECT_REL);
|
|
686
|
+
const project = blob.kind === 'bytes' ? projectConfig.parseProjectBytes(blob.bytes)
|
|
687
|
+
: blob.kind === 'absent' ? PROJECT_ABSENT : PROJECT_INVALID;
|
|
688
|
+
return withRetiredPolicy(projectReading(project), () => catFilePolicyPresence(ctx, root, revision));
|
|
631
689
|
}
|
|
632
690
|
|
|
633
691
|
/**
|
|
634
692
|
* Steps 6 and 7 (offline, D known): T, the local tracking copy of the default
|
|
635
|
-
* branch's
|
|
693
|
+
* branch's files. The ref check decides whether T can stand for the default branch
|
|
636
694
|
* at all — a ref git ANSWERS is missing is "B unknown" (null), which a failed blob
|
|
637
695
|
* read could not tell apart from "file absent". A ref check git does not answer is
|
|
638
696
|
* not knowing, so T is invalid (it raises) rather than unknown (it would not).
|
|
@@ -640,14 +698,14 @@ function catFilePolicy(ctx, root, revision) {
|
|
|
640
698
|
* @param {CallContext} ctx
|
|
641
699
|
* @param {string} root
|
|
642
700
|
* @param {string} ref
|
|
643
|
-
* @returns {
|
|
701
|
+
* @returns {SourceReading | null}
|
|
644
702
|
*/
|
|
645
|
-
function
|
|
703
|
+
function trackingSource(ctx, root, ref) {
|
|
646
704
|
const trackingRef = 'refs/remotes/origin/' + ref;
|
|
647
705
|
const r = runCall(ctx, 'git', ['rev-parse', '--verify', '--quiet', trackingRef], root,
|
|
648
706
|
GIT_LOCAL_TIMEOUT_MS, LINE_MAX_BUFFER);
|
|
649
|
-
if (r.ok) return
|
|
650
|
-
return answered(r) ? null : INVALID;
|
|
707
|
+
if (r.ok) return revisionSource(ctx, root, trackingRef);
|
|
708
|
+
return answered(r) ? null : { evidence: INVALID, compliance: false };
|
|
651
709
|
}
|
|
652
710
|
|
|
653
711
|
// ---------------------------------------------------------------------------
|
|
@@ -664,27 +722,35 @@ function trackingPolicy(ctx, root, ref) {
|
|
|
664
722
|
* head: ParsedPolicy | null,
|
|
665
723
|
* compliance: Policy,
|
|
666
724
|
* }} Facts
|
|
667
|
-
* remote
|
|
668
|
-
* tracking
|
|
669
|
-
*
|
|
670
|
-
*
|
|
725
|
+
* remote R — set iff reachable
|
|
726
|
+
* tracking T — set iff offline and refs/remotes/origin/<D> exists (or git
|
|
727
|
+
* could not answer whether it does, or origin/HEAD is unreadable —
|
|
728
|
+
* then invalid)
|
|
729
|
+
* head H — set iff B (R online, T offline) is known
|
|
730
|
+
* compliance stricter(C_machine, C_repo) — D-COMPLIANCE-REPO-FLOOR
|
|
671
731
|
*/
|
|
672
732
|
|
|
673
733
|
/**
|
|
674
|
-
* Run the fixed call sequence. The only calls ever made, in order:
|
|
675
|
-
* git rev-parse --show-toplevel
|
|
734
|
+
* Run the fixed call sequence (D-POLICY-PROBE). The only calls ever made, in order:
|
|
735
|
+
* git rev-parse --show-toplevel (cwd = <dir>)
|
|
676
736
|
* gh api repos/{owner}/{repo} --jq .default_branch
|
|
677
|
-
* gh api --method GET …/contents/.devflow/
|
|
678
|
-
*
|
|
679
|
-
*
|
|
680
|
-
*
|
|
681
|
-
* git
|
|
737
|
+
* gh api --method GET …/contents/.devflow/project.json (the probe named D)
|
|
738
|
+
* gh api --method GET …/contents/.devflow/policy.json (… and project.json is
|
|
739
|
+
* a 404 or has no evidence;
|
|
740
|
+
* presence only)
|
|
741
|
+
* git ls-remote --symref origin HEAD (offline, D unknown)
|
|
742
|
+
* git symbolic-ref --quiet refs/remotes/origin/HEAD (… and ls-remote named none)
|
|
743
|
+
* git rev-parse --verify --quiet refs/remotes/origin/D (offline, D known)
|
|
744
|
+
* git cat-file blob refs/remotes/origin/D:…/project.json (that ref exists)
|
|
745
|
+
* git cat-file blob refs/remotes/origin/D:…/policy.json (… no evidence there; presence)
|
|
746
|
+
* git cat-file blob HEAD:…/project.json (B known)
|
|
747
|
+
* git cat-file blob HEAD:…/policy.json (… no evidence there; presence)
|
|
682
748
|
*
|
|
683
749
|
* Returns null when git cannot say whether <dir> is in a repository at all (see
|
|
684
750
|
* gitToplevel): nothing below can be trusted then, and resolve() fails closed.
|
|
685
751
|
*
|
|
686
752
|
* @param {string} dir
|
|
687
|
-
* @param {Policy} compliance
|
|
753
|
+
* @param {Policy} compliance C_machine
|
|
688
754
|
* @param {ExecFn} exec
|
|
689
755
|
* @returns {Facts | null}
|
|
690
756
|
*/
|
|
@@ -702,20 +768,37 @@ function gatherFacts(dir, compliance, exec) {
|
|
|
702
768
|
}
|
|
703
769
|
const root = toplevel.root;
|
|
704
770
|
|
|
705
|
-
const worktree =
|
|
771
|
+
const worktree = readWorktreeSource(root);
|
|
706
772
|
let ref = probeDefaultBranch(ctx, root);
|
|
707
|
-
const remote = ref === null ? null :
|
|
773
|
+
const remote = ref === null ? null : remoteSource(ctx, root, ref);
|
|
708
774
|
const reachable = remote !== null;
|
|
709
775
|
|
|
776
|
+
/** @type {SourceReading | null} */
|
|
710
777
|
let tracking = null;
|
|
711
778
|
if (!reachable) {
|
|
712
779
|
if (ref === null) ref = lsRemoteDefaultBranch(ctx, root);
|
|
713
|
-
if (ref
|
|
780
|
+
if (ref === null) {
|
|
781
|
+
const originHead = localOriginHead(ctx, root);
|
|
782
|
+
if (originHead.kind === 'ref') ref = originHead.ref;
|
|
783
|
+
if (originHead.kind === 'unreadable') tracking = { evidence: INVALID, compliance: false };
|
|
784
|
+
}
|
|
785
|
+
if (ref !== null) tracking = trackingSource(ctx, root, ref);
|
|
714
786
|
}
|
|
715
787
|
|
|
716
788
|
const baseKnown = reachable || tracking !== null;
|
|
717
|
-
const head = baseKnown ?
|
|
718
|
-
|
|
789
|
+
const head = baseKnown ? revisionSource(ctx, root, 'HEAD') : null;
|
|
790
|
+
const repoCompliance = worktree.compliance
|
|
791
|
+
|| (remote !== null && remote.compliance)
|
|
792
|
+
|| (tracking !== null && tracking.compliance);
|
|
793
|
+
return {
|
|
794
|
+
reachable,
|
|
795
|
+
ref,
|
|
796
|
+
remote: remote === null ? null : remote.evidence,
|
|
797
|
+
worktree: worktree.evidence,
|
|
798
|
+
tracking: tracking === null ? null : tracking.evidence,
|
|
799
|
+
head: head === null ? null : head.evidence,
|
|
800
|
+
compliance: stricter(compliance, repoCompliance ? 'required' : 'standard'),
|
|
801
|
+
};
|
|
719
802
|
}
|
|
720
803
|
|
|
721
804
|
// ---------------------------------------------------------------------------
|
|
@@ -1051,15 +1134,12 @@ module.exports = Object.freeze({
|
|
|
1051
1134
|
WARNINGS,
|
|
1052
1135
|
EXIT_CODES,
|
|
1053
1136
|
MECHANISM_INPUTS,
|
|
1054
|
-
MAX_POLICY_BYTES,
|
|
1055
1137
|
SAFE_REF_RE,
|
|
1056
1138
|
OUTPUT_LINE_RE,
|
|
1057
1139
|
FAIL_CLOSED_LINE,
|
|
1058
1140
|
parseArgs,
|
|
1059
|
-
parsePolicyBytes,
|
|
1060
1141
|
complianceDefault,
|
|
1061
1142
|
resolve,
|
|
1062
1143
|
formatLine,
|
|
1063
|
-
serializePolicy,
|
|
1064
1144
|
main,
|
|
1065
1145
|
});
|