devflow-kit 3.11.0 → 3.12.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/CHANGELOG.md +73 -0
- package/dist/agents/architect.md +91 -0
- package/dist/agents/code.md +21 -25
- package/dist/agents/debug.md +22 -50
- package/dist/agents/design.md +9 -43
- package/dist/agents/fix.md +15 -29
- package/dist/agents/git.md +19 -22
- package/dist/agents/harden.md +14 -19
- package/dist/agents/knowledge.md +11 -19
- package/dist/agents/refactor.md +9 -14
- package/dist/agents/research.md +11 -34
- package/dist/agents/review.md +22 -52
- package/dist/agents/skim.md +7 -17
- package/dist/agents/triage.md +7 -14
- package/dist/cli/commands/uninstall.js +1 -1
- package/dist/commands/bug-analysis.md +32 -63
- package/dist/commands/code-review.md +39 -88
- package/dist/commands/debug.md +32 -82
- package/dist/commands/dynamic-build.md +157 -211
- package/dist/commands/dynamic-plan.md +71 -93
- package/dist/commands/dynamic-profile.md +7 -155
- package/dist/commands/dynamic-tickets.md +65 -186
- package/dist/commands/explore.md +27 -70
- package/dist/commands/implement.md +101 -189
- package/dist/commands/plan.md +61 -108
- package/dist/commands/release.md +26 -92
- package/dist/commands/research.md +37 -106
- package/dist/commands/resolve.md +65 -129
- package/dist/commands/self-review.md +36 -100
- package/dist/compliance-off/agents/git.md +15 -18
- package/dist/compliance-off/agents/triage.md +7 -14
- package/dist/compliance-off/commands/bug-analysis.md +32 -63
- package/dist/compliance-off/commands/code-review.md +35 -83
- package/dist/compliance-off/commands/dynamic-build.md +151 -205
- package/dist/compliance-off/commands/dynamic-tickets.md +65 -186
- package/dist/compliance-off/commands/implement.md +97 -183
- package/dist/compliance-off/commands/plan.md +61 -108
- package/dist/compliance-off/commands/release.md +26 -91
- package/dist/compliance-off/commands/resolve.md +55 -129
- package/dist/compliance-off/skills/git/references/tracker/github/ensure-pr-ready.md +3 -3
- package/dist/compliance-off/skills/git/references/tracker/jira/ensure-pr-ready.md +3 -3
- package/dist/compliance-off/skills/git/references/tracker/linear/ensure-pr-ready.md +3 -3
- package/dist/core/plugins.js +3 -3
- package/dist/curation-compliance-off/agents/triage.md +7 -13
- package/dist/curation-compliance-off/commands/bug-analysis.md +22 -42
- package/dist/curation-compliance-off/commands/code-review.md +26 -63
- package/dist/curation-compliance-off/commands/dynamic-build.md +144 -194
- package/dist/curation-compliance-off/commands/dynamic-tickets.md +54 -173
- package/dist/curation-compliance-off/commands/implement.md +89 -164
- package/dist/curation-compliance-off/commands/plan.md +52 -87
- package/dist/curation-compliance-off/commands/release.md +18 -79
- package/dist/curation-compliance-off/commands/resolve.md +46 -109
- package/dist/curation-off/agents/architect.md +85 -0
- package/dist/curation-off/agents/code.md +20 -23
- package/dist/curation-off/agents/debug.md +20 -48
- package/dist/curation-off/agents/design.md +8 -41
- package/dist/curation-off/agents/fix.md +14 -27
- package/dist/curation-off/agents/harden.md +13 -18
- package/dist/curation-off/agents/knowledge.md +9 -17
- package/dist/curation-off/agents/refactor.md +8 -12
- package/dist/curation-off/agents/research.md +9 -32
- package/dist/curation-off/agents/review.md +11 -40
- package/dist/curation-off/agents/skim.md +7 -17
- package/dist/curation-off/agents/triage.md +7 -13
- package/dist/curation-off/commands/bug-analysis.md +22 -42
- package/dist/curation-off/commands/code-review.md +30 -68
- package/dist/curation-off/commands/debug.md +24 -63
- package/dist/curation-off/commands/dynamic-build.md +150 -200
- package/dist/curation-off/commands/dynamic-plan.md +60 -80
- package/dist/curation-off/commands/dynamic-tickets.md +54 -173
- package/dist/curation-off/commands/explore.md +18 -50
- package/dist/curation-off/commands/implement.md +93 -170
- package/dist/curation-off/commands/plan.md +52 -87
- package/dist/curation-off/commands/release.md +18 -80
- package/dist/curation-off/commands/research.md +26 -84
- package/dist/curation-off/commands/resolve.md +56 -109
- package/dist/curation-off/commands/self-review.md +27 -79
- package/dist/hud/components/version-badge.js +1 -1
- package/dist/skills/git/references/pr/update-pr-evidence.md +2 -1
- package/dist/skills/git/references/tracker/github/ensure-pr-ready.md +3 -3
- package/dist/skills/git/references/tracker/jira/ensure-pr-ready.md +3 -3
- package/dist/skills/git/references/tracker/linear/ensure-pr-ready.md +3 -3
- package/dist/targets/claude-code/curation-install.js +2 -2
- package/dist/variant-axes.json +5 -1
- package/package.json +1 -1
- package/src/assets/agents/_partials/_code.mds +3 -5
- package/src/assets/agents/_partials/_design.mds +86 -0
- package/src/assets/agents/architect.mds +45 -0
- package/src/assets/agents/code.mds +18 -20
- package/src/assets/agents/curate.md +26 -35
- package/src/assets/agents/debug.mds +22 -50
- package/src/assets/agents/design.mds +11 -114
- package/src/assets/agents/evaluate.md +6 -16
- package/src/assets/agents/explore.md +1 -2
- package/src/assets/agents/fix.mds +12 -24
- package/src/assets/agents/git.mds +20 -23
- package/src/assets/agents/harden.mds +14 -19
- package/src/assets/agents/knowledge.mds +10 -38
- package/src/assets/agents/polish.md +13 -39
- package/src/assets/agents/refactor.mds +6 -9
- package/src/assets/agents/research.mds +11 -34
- package/src/assets/agents/review.mds +12 -84
- package/src/assets/agents/skim.mds +7 -17
- package/src/assets/agents/synthesize.md +3 -21
- package/src/assets/agents/test.md +31 -32
- package/src/assets/agents/tracker.md +116 -229
- package/src/assets/agents/triage.mds +7 -26
- package/src/assets/agents/validate.md +1 -3
- package/src/assets/commands/_partials/_compliance.mds +2 -2
- package/src/assets/commands/_partials/_decisions.mds +17 -24
- package/src/assets/commands/_partials/_docs_root.mds +31 -10
- package/src/assets/commands/_partials/_engine.mds +36 -56
- package/src/assets/commands/_partials/_evidence_policy.mds +1 -1
- package/src/assets/commands/_partials/_factory.mds +10 -104
- package/src/assets/commands/_partials/_knowledge.mds +48 -52
- package/src/assets/commands/_partials/_plan_contract.mds +2 -2
- package/src/assets/commands/_partials/_preamble.mds +21 -21
- package/src/assets/commands/_partials/_publication.mds +3 -3
- package/src/assets/commands/_partials/_roster.mds +12 -13
- package/src/assets/commands/_partials/_settings.mds +6 -3
- package/src/assets/commands/_partials/_ticket_template.mds +3 -7
- package/src/assets/commands/_partials/_tracker.mds +10 -5
- package/src/assets/commands/_partials/_wave.mds +23 -32
- package/src/assets/commands/bug-analysis.mds +3 -7
- package/src/assets/commands/code-review.mds +12 -33
- package/src/assets/commands/debug.mds +5 -47
- package/src/assets/commands/dynamic-build.mds +48 -70
- package/src/assets/commands/dynamic-plan.mds +23 -42
- package/src/assets/commands/dynamic-profile.mds +7 -57
- package/src/assets/commands/dynamic-tickets.mds +9 -29
- package/src/assets/commands/explore.mds +5 -44
- package/src/assets/commands/implement.mds +77 -119
- package/src/assets/commands/plan.mds +27 -57
- package/src/assets/commands/release.mds +4 -62
- package/src/assets/commands/research.mds +9 -55
- package/src/assets/commands/resolve.mds +40 -87
- package/src/assets/commands/self-review.mds +5 -45
- package/src/assets/mds/git/_ops_pr.mds +1 -0
- package/src/assets/mds/git/_ops_steps.mds +4 -4
- package/src/assets/mds/git/_pr.mds +1 -1
- package/src/assets/rules/engineering.md +1 -1
- package/src/assets/rules/quality.md +2 -2
- package/src/assets/scripts/ci-wait.cjs +154 -19
- package/src/assets/scripts/hooks/assets/orchestrator-charter.md +3 -3
- package/src/assets/scripts/hooks/preamble +2 -3
- package/src/assets/scripts/pr-evidence.cjs +1 -0
- package/src/assets/scripts/resolve-evidence-policy.cjs +14 -13
- package/src/assets/scripts/resolve-root.cjs +190 -0
- package/src/assets/scripts/resolve-settings.cjs +14 -9
- package/src/assets/scripts/verify-evidence.cjs +551 -25
- package/src/assets/skills/accessibility/SKILL.md +28 -95
- package/src/assets/skills/agent-references/SKILL.md +2 -4
- package/src/assets/skills/agent-references/references/commands/implement-evidence-asks.md +1 -1
- package/src/assets/skills/agent-references/references/commands/implement-levels.md +4 -7
- package/src/assets/skills/agent-references/references/commands/implement-revalidate.md +1 -1
- package/src/assets/skills/agent-references/references/commands/plan-levels.md +2 -2
- package/src/assets/skills/apply-decisions/SKILL.md +5 -6
- package/src/assets/skills/apply-feature-knowledge/SKILL.md +3 -13
- package/src/assets/skills/architecture/SKILL.md +29 -105
- package/src/assets/skills/boundary-validation/SKILL.md +27 -78
- package/src/assets/skills/complexity/SKILL.md +8 -104
- package/src/assets/skills/compliance/SKILL.md +9 -67
- package/src/assets/skills/consistency/SKILL.md +7 -107
- package/src/assets/skills/database/SKILL.md +7 -72
- package/src/assets/skills/dependencies/SKILL.md +9 -109
- package/src/assets/skills/dependencies/references/detection.md +10 -0
- package/src/assets/skills/dependency-research/SKILL.md +9 -63
- package/src/assets/skills/design-review/SKILL.md +14 -37
- package/src/assets/skills/docs-framework/SKILL.md +4 -47
- package/src/assets/skills/documentation/SKILL.md +7 -71
- package/src/assets/skills/documentation/references/patterns.md +1 -1
- package/src/assets/skills/documentation/references/violations.md +1 -1
- package/src/assets/skills/feature-knowledge/SKILL.md +19 -222
- package/src/assets/skills/feature-knowledge/references/new-kb.md +128 -0
- package/src/assets/skills/gap-analysis/SKILL.md +1 -31
- package/src/assets/skills/git/SKILL.md +2 -22
- package/src/assets/skills/go/SKILL.md +24 -113
- package/src/assets/skills/java/SKILL.md +12 -98
- package/src/assets/skills/patterns/SKILL.md +8 -93
- package/src/assets/skills/performance/SKILL.md +29 -88
- package/src/assets/skills/python/SKILL.md +22 -120
- package/src/assets/skills/qa/SKILL.md +16 -54
- package/src/assets/skills/qa/references/browser-testing.md +0 -24
- package/src/assets/skills/qa/references/sources.md +2 -2
- package/src/assets/skills/quality-gates/SKILL.md +13 -100
- package/src/assets/skills/react/SKILL.md +33 -95
- package/src/assets/skills/regression/SKILL.md +8 -73
- package/src/assets/skills/reliability/SKILL.md +11 -120
- package/src/assets/skills/research-codebase/SKILL.md +7 -49
- package/src/assets/skills/research-competitor/SKILL.md +4 -39
- package/src/assets/skills/research-external/SKILL.md +5 -43
- package/src/assets/skills/research-market/SKILL.md +5 -43
- package/src/assets/skills/research-technology/SKILL.md +8 -68
- package/src/assets/skills/review-methodology/SKILL.md +10 -56
- package/src/assets/skills/rust/SKILL.md +19 -109
- package/src/assets/skills/security/SKILL.md +39 -66
- package/src/assets/skills/software-design/SKILL.md +20 -105
- package/src/assets/skills/software-design/references/patterns.md +84 -2
- package/src/assets/skills/software-design/references/violations.md +1 -1
- package/src/assets/skills/test-driven-development/SKILL.md +32 -107
- package/src/assets/skills/test-driven-development/references/rationalization-prevention.md +33 -1
- package/src/assets/skills/testing/SKILL.md +35 -53
- package/src/assets/skills/typescript/SKILL.md +23 -102
- package/src/assets/skills/ui-design/SKILL.md +26 -74
- package/src/assets/skills/worktree-support/SKILL.md +4 -10
- package/dist/curation-off/commands/dynamic-profile.md +0 -214
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
// node verify-evidence.cjs publish --pr <n> [--publication auto|full|off|stub] [--evidence <file>]
|
|
20
20
|
// node verify-evidence.cjs readiness --pr <n> [--require-non-author-approval true|false]
|
|
21
21
|
// [--evidence <file>] [--out <file>]
|
|
22
|
+
// node verify-evidence.cjs claim --evidence <file> --head <sha> [--plan <file>]
|
|
23
|
+
// gate | TP-<n>=<PASS|FAIL|SKIP>[:<exit>] ...
|
|
22
24
|
//
|
|
23
25
|
// stdout (D-VERIFY-STDOUT) is empty or exactly one of these shapes, each closed-vocabulary
|
|
24
26
|
// (the render block's TP lines come from the caller's own plan file):
|
|
@@ -33,10 +35,14 @@
|
|
|
33
35
|
// posted:<yes|no|n/a> body:<same|changed> (one line; wrapped here)
|
|
34
36
|
// splice SPLICE ok|resplice (exit 0) or SPLICE conflict|malformed|oversize (exit 5)
|
|
35
37
|
// readback READBACK ok (exit 0) or READBACK mismatch (exit 5)
|
|
36
|
-
// publish the `## PR Evidence` block — the heading, the EVIDENCE line,
|
|
38
|
+
// publish the `## PR Evidence` block — the heading, the EVIDENCE line, the line
|
|
39
|
+
// `Test plan: <v>/<total> verified (VERIFIED-CI <a>, ATTESTED-LOCAL <b>)`
|
|
40
|
+
// (v is a + b, all three read from the EVIDENCE line), one
|
|
37
41
|
// `**Body**: … · **Comment**: …` line — then one `TRACEABILITY: DEGRADED
|
|
38
42
|
// (<reason>)` line per degraded part (exit 0), or exit 2 with one line:
|
|
39
|
-
// `TRACEABILITY: DEGRADED (evidence unavailable)` when it could not run
|
|
43
|
+
// `TRACEABILITY: DEGRADED (evidence unavailable)` when it could not run (a
|
|
44
|
+
// usage error, an internal error, a failed read and a block the output gate
|
|
45
|
+
// refuses all end this way, so a `publish` never ends with empty stdout),
|
|
40
46
|
// `TRACEABILITY: DEGRADED (compliance off)` when the machine manifest does not
|
|
41
47
|
// say compliance is enabled, or `TRACEABILITY: DEGRADED (compliance state
|
|
42
48
|
// unreadable)` when that manifest cannot be read. The last two come before
|
|
@@ -55,10 +61,34 @@
|
|
|
55
61
|
// is removed before the first read, and a block that does not parse or a
|
|
56
62
|
// write that fails leaves no out file. The one exception is a path that
|
|
57
63
|
// git tracks (below), which is left as it is
|
|
64
|
+
// claim D-CLAIM-APPEND: appends claim lines to the `## Claims` section of the
|
|
65
|
+
// `--evidence` file. `gate` writes `- gate:validate PASS sha:<head>
|
|
66
|
+
// by:validate`. Each `TP-<n>=<outcome>[:<exit>]` writes `- TP-<n> <outcome>
|
|
67
|
+
// sha:<head> by:test[ exit:<exit>]`. `--head` is one 40-hex SHA, else
|
|
68
|
+
// nothing is written. The `exit:` part is written only for a number from 0
|
|
69
|
+
// to 255. A TP that the plan does not list gets no line. The plan is the
|
|
70
|
+
// `## Test Plan` of the `--plan` file, or of the evidence file when `--plan`
|
|
71
|
+
// is absent. The `## Claims` section is created when absent, at the end
|
|
72
|
+
// of the file; when it is not the last section the lines go at its end
|
|
73
|
+
// (D-CLAIM-INSERT). A missing folder inside the working directory is
|
|
74
|
+
// created, and so is the file (D-CLAIM-FOLDER). The write is one append of
|
|
75
|
+
// every line, or, for a Claims section that is not last, one whole-file
|
|
76
|
+
// replace through a sibling temporary file that keeps the file's mode. The
|
|
77
|
+
// parser of pr-evidence.cjs accepts each line before it. It prints one
|
|
78
|
+
// line, exit 0:
|
|
79
|
+
// `CLAIM appended:<n> PASS:<n> FAIL:<n> SKIP:<n> skipped:<n>`
|
|
80
|
+
// (`skipped` counts the TPs that got no line), or exit 2 with
|
|
81
|
+
// `CLAIM refused (<reason>)`, the reason being `head not one sha`,
|
|
82
|
+
// `evidence file tracked` (git tracks it, or cannot say it does not),
|
|
83
|
+
// `evidence file unusable` (a file that does not parse, or a folder that is
|
|
84
|
+
// missing outside the working directory) or `plan file unusable`. A refusal
|
|
85
|
+
// writes nothing. A folder the run made is removed again when the run
|
|
86
|
+
// leaves no file in it
|
|
58
87
|
// A git-tracked file counts as absent, like a tracked `.devflow/config.json`
|
|
59
88
|
// (D-TRACKED-RESULT). `verify`, `publish` and `readiness` ask git about an `--evidence`
|
|
60
89
|
// file before they read it, and about `--block-out`, `--comment-out`, `--stale-out` and
|
|
61
|
-
// `--out` before they write or remove one.
|
|
90
|
+
// `--out` before they write or remove one. `claim` asks about its `--evidence` and
|
|
91
|
+
// `--plan` files, and refuses both when either is not clearly untracked. Only a clear "not tracked" answer lets the
|
|
62
92
|
// file be read or written. A tracked `--evidence` file, or one git cannot answer for, is
|
|
63
93
|
// dropped: the run goes on exactly as if `--evidence` was not given, and the stderr
|
|
64
94
|
// summary carries the note `evidence-ignored`. A tracked out path, or one git cannot
|
|
@@ -68,14 +98,16 @@
|
|
|
68
98
|
//
|
|
69
99
|
// Exit codes:
|
|
70
100
|
// 0 ok
|
|
71
|
-
// 1 usage error — stdout entirely empty, usage on stderr
|
|
101
|
+
// 1 usage error — stdout entirely empty, usage on stderr (a `publish` usage error is
|
|
102
|
+
// exit 2 with the evidence unavailable line instead)
|
|
72
103
|
// 2 input unusable — a file could not be read (missing, not a regular file,
|
|
73
104
|
// oversize, not UTF-8), the evidence file or the state directory is
|
|
74
105
|
// malformed, or the PR body's test-plan block cannot serve as the plan.
|
|
75
106
|
// For `publish` and `readiness` (D-OP-SCRIPT-POINTER) it is the DEGRADED
|
|
76
107
|
// result: the operation could not read what it needs, and the stdout above
|
|
77
|
-
// says so
|
|
78
|
-
// 3 output write failed — no stdout line is printed
|
|
108
|
+
// says so. For `claim` it is the refusal line above
|
|
109
|
+
// 3 output write failed — no stdout line is printed (a `claim` write leaves the
|
|
110
|
+
// evidence file as it was)
|
|
79
111
|
// 4 remote or internal failure before any verdict — `gh pr view` failed or
|
|
80
112
|
// answered something unusable, or an unexpected internal error
|
|
81
113
|
// 5 output gate refused — `check` found the file invalid, the EVIDENCE line
|
|
@@ -223,6 +255,12 @@ const DECISIVE_REVIEW_STATES = new Set(['APPROVED', 'CHANGES_REQUESTED', 'DISMIS
|
|
|
223
255
|
const SPLICE_LINE_RE = /^SPLICE (?<outcome>ok|resplice|conflict|malformed|oversize)$/;
|
|
224
256
|
const READBACK_LINE_RE = /^READBACK (?<outcome>ok|mismatch)$/;
|
|
225
257
|
|
|
258
|
+
/** D-CLAIM-APPEND: the stdout shapes of `claim`, and the closed set of its refusal reasons. */
|
|
259
|
+
const CLAIM_APPENDED_RE = /^CLAIM appended:(?<n>[0-9]{1,3}) PASS:(?<pass>[0-9]{1,3}) FAIL:(?<fail>[0-9]{1,3}) SKIP:(?<skip>[0-9]{1,3}) skipped:(?<skipped>[0-9]{1,3})$/;
|
|
260
|
+
const CLAIM_REFUSAL_RE = /^CLAIM refused \((?<reason>head not one sha|evidence file tracked|evidence file unusable|plan file unusable)\)$/;
|
|
261
|
+
/** One `claim` argument: the TP, its outcome, and an exit the script keeps only as a number from 0 to 255. */
|
|
262
|
+
const CLAIM_ARG_RE = /^TP-(?<n>[1-9][0-9]{0,2})=(?<outcome>PASS|FAIL|SKIP)(?::(?<exit>[^\s]{1,12}))?$/;
|
|
263
|
+
|
|
226
264
|
const USAGE = [
|
|
227
265
|
'Usage: node verify-evidence.cjs check tp|block|exceptions|wave <file>',
|
|
228
266
|
' node verify-evidence.cjs check wave --links-only <file>',
|
|
@@ -234,6 +272,8 @@ const USAGE = [
|
|
|
234
272
|
' node verify-evidence.cjs publish --pr <n> [--publication auto|full|off|stub] [--evidence <file>]',
|
|
235
273
|
' node verify-evidence.cjs readiness --pr <n> [--require-non-author-approval true|false]',
|
|
236
274
|
' [--evidence <file>] [--out <file>]',
|
|
275
|
+
' node verify-evidence.cjs claim --evidence <file> --head <sha> [--plan <file>]',
|
|
276
|
+
' gate | TP-<n>=<PASS|FAIL|SKIP>[:<exit>] ...',
|
|
237
277
|
].join('\n');
|
|
238
278
|
|
|
239
279
|
// ---------------------------------------------------------------------------
|
|
@@ -253,11 +293,12 @@ const USAGE = [
|
|
|
253
293
|
* @typedef {{ ghCalls: number, runViews: number, throttled: boolean }} Budget
|
|
254
294
|
*
|
|
255
295
|
* @typedef {{ exec: ExecFn, env: NodeJS.ProcessEnv, cwd: string, now: () => number,
|
|
256
|
-
* deadline: number, budget: Budget, notes: Set<string>,
|
|
257
|
-
* ghCap?: number }} Io
|
|
296
|
+
* deadline: number, budget: Budget, notes: Set<string>, made: string[],
|
|
297
|
+
* stderr: (text: string) => void, ghCap?: number }} Io
|
|
258
298
|
* The imperative shell's one context: every call goes through it, so every
|
|
259
299
|
* bound is counted in one place. `notes` holds closed tokens for the stderr
|
|
260
|
-
* summary, never input bytes. `
|
|
300
|
+
* summary, never input bytes. `made` holds the folders a `claim` run created, deepest
|
|
301
|
+
* first (D-CLAIM-FOLDER). `ghCap` replaces CAPS.GH_CALLS for a stage that
|
|
261
302
|
* carries a budget of its own (`publish`'s writes).
|
|
262
303
|
*
|
|
263
304
|
* @typedef {{ login: string, association: string, body: string, viewerDidAuthor: boolean, commentId: string }} PrComment
|
|
@@ -290,7 +331,9 @@ const USAGE = [
|
|
|
290
331
|
* | { kind: 'readback', pr: number, expect: string }
|
|
291
332
|
* | { kind: 'publish', pr: number, publication: string, evidence: string | null }
|
|
292
333
|
* | { kind: 'readiness', pr: number, requireNonAuthor: 'true' | 'false' | 'unrecognised',
|
|
293
|
-
* evidence: string | null, out: string | null }
|
|
334
|
+
* evidence: string | null, out: string | null }
|
|
335
|
+
* | { kind: 'claim', evidence: string, head: string, plan: string | null, gate: boolean,
|
|
336
|
+
* tps: readonly { id: number, outcome: 'PASS' | 'FAIL' | 'SKIP', exit: string | null }[] }} ParsedArgs
|
|
294
337
|
*/
|
|
295
338
|
|
|
296
339
|
/** @type {ParsedArgs} */
|
|
@@ -423,9 +466,45 @@ function parseArgs(argv) {
|
|
|
423
466
|
// An absent flag is the ladder's "unrecognised": it can never read READY.
|
|
424
467
|
return { kind: 'readiness', pr, requireNonAuthor: needed === null ? 'unrecognised' : needed, evidence, out };
|
|
425
468
|
}
|
|
469
|
+
if (sub === 'claim') return parseClaimArgs(tail);
|
|
426
470
|
return USAGE_ARGS;
|
|
427
471
|
}
|
|
428
472
|
|
|
473
|
+
/**
|
|
474
|
+
* D-CLAIM-APPEND: `claim` takes its flags first, then either the one word `gate` or
|
|
475
|
+
* one to CAPS.TPS `TP-<n>=<outcome>[:<exit>]` arguments, each TP at most once. A bad
|
|
476
|
+
* flag or argument is a usage error. `--head` is not checked here: a value that is not
|
|
477
|
+
* one SHA is a refusal with a line, since the caller passes what its report shows.
|
|
478
|
+
*
|
|
479
|
+
* @param {readonly string[]} tail
|
|
480
|
+
* @returns {ParsedArgs}
|
|
481
|
+
*/
|
|
482
|
+
function parseClaimArgs(tail) {
|
|
483
|
+
let flagCount = 0;
|
|
484
|
+
while (flagCount < tail.length && tail[flagCount].startsWith('--')) flagCount += 2;
|
|
485
|
+
const f = parseFlags(tail.slice(0, flagCount), { '--evidence': 'value', '--head': 'value', '--plan': 'value' });
|
|
486
|
+
const evidence = f === null ? null : flagValue(f, '--evidence');
|
|
487
|
+
const head = f === null ? null : flagValue(f, '--head');
|
|
488
|
+
const plan = f === null ? null : flagValue(f, '--plan');
|
|
489
|
+
const items = tail.slice(flagCount);
|
|
490
|
+
if (evidence === null || head === null || items.length === 0 || items.length > CAPS.TPS) return USAGE_ARGS;
|
|
491
|
+
if (items.length === 1 && items[0] === 'gate') {
|
|
492
|
+
return plan === null ? { kind: 'claim', evidence, head, plan, gate: true, tps: [] } : USAGE_ARGS;
|
|
493
|
+
}
|
|
494
|
+
const tps = [];
|
|
495
|
+
for (const item of items) {
|
|
496
|
+
const m = CLAIM_ARG_RE.exec(item);
|
|
497
|
+
if (m === null || m.groups === undefined) return USAGE_ARGS;
|
|
498
|
+
tps.push({
|
|
499
|
+
id: Number(m.groups.n),
|
|
500
|
+
outcome: /** @type {'PASS' | 'FAIL' | 'SKIP'} */ (m.groups.outcome),
|
|
501
|
+
exit: m.groups.exit === undefined ? null : m.groups.exit,
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
if (new Set(tps.map(tp => tp.id)).size !== tps.length) return USAGE_ARGS;
|
|
505
|
+
return { kind: 'claim', evidence, head, plan, gate: false, tps };
|
|
506
|
+
}
|
|
507
|
+
|
|
429
508
|
// ---------------------------------------------------------------------------
|
|
430
509
|
// Bounded file I/O
|
|
431
510
|
// ---------------------------------------------------------------------------
|
|
@@ -516,6 +595,97 @@ function writeTextFile(filePath, text) {
|
|
|
516
595
|
}
|
|
517
596
|
}
|
|
518
597
|
|
|
598
|
+
/**
|
|
599
|
+
* Append `text` to `filePath` in one write, creating the file when it is absent. The same
|
|
600
|
+
* refusals as writeTextFile apply: a path that exists and is not a regular file, a link
|
|
601
|
+
* put in its place (O_NOFOLLOW), a FIFO (O_NONBLOCK). A write that does not complete
|
|
602
|
+
* cuts the file back to its length before the append, or removes the file when this call
|
|
603
|
+
* created it, so a failed append leaves no part of `text`. Returns whether it was written.
|
|
604
|
+
*
|
|
605
|
+
* @param {string} filePath
|
|
606
|
+
* @param {string} text
|
|
607
|
+
* @returns {boolean}
|
|
608
|
+
*/
|
|
609
|
+
function appendTextFile(filePath, text) {
|
|
610
|
+
let fd;
|
|
611
|
+
let created = false;
|
|
612
|
+
try {
|
|
613
|
+
const st = fs.lstatSync(filePath, { throwIfNoEntry: false });
|
|
614
|
+
if (st !== undefined && !st.isFile()) return false;
|
|
615
|
+
created = st === undefined;
|
|
616
|
+
const c = fs.constants;
|
|
617
|
+
fd = fs.openSync(filePath, c.O_WRONLY | c.O_APPEND | c.O_CREAT | (c.O_NOFOLLOW || 0) | (c.O_NONBLOCK || 0), 0o600);
|
|
618
|
+
const before = fs.fstatSync(fd);
|
|
619
|
+
if (!before.isFile()) return false;
|
|
620
|
+
const bytes = Buffer.from(text, 'utf8');
|
|
621
|
+
try {
|
|
622
|
+
if (fs.writeSync(fd, bytes) === bytes.length) return true;
|
|
623
|
+
} catch (_) { /* cut back below */ }
|
|
624
|
+
try { fs.ftruncateSync(fd, before.size); } catch (_) { /* the write already failed */ }
|
|
625
|
+
if (created) fs.unlinkSync(filePath);
|
|
626
|
+
return false;
|
|
627
|
+
} catch (_) {
|
|
628
|
+
return false;
|
|
629
|
+
} finally {
|
|
630
|
+
if (fd !== undefined) {
|
|
631
|
+
try { fs.closeSync(fd); } catch (_) { /* the write already decided */ }
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* Replace the regular file `filePath` with `text` and keep its `mode`: the text goes to a
|
|
638
|
+
* sibling temporary file, in the same folder, which a rename then puts in the file's place.
|
|
639
|
+
* It refuses first what appendTextFile refuses, so the two writes of a claim agree: a file
|
|
640
|
+
* this process may not write, a link put in its place (O_NOFOLLOW), a FIFO (O_NONBLOCK) or
|
|
641
|
+
* any other object that is not a regular file. The probe opens the file for an append and
|
|
642
|
+
* writes nothing. A write that fails leaves the file as it was and the temporary file
|
|
643
|
+
* removed, so a failed replace never truncates it. The temporary file is created new
|
|
644
|
+
* (O_EXCL) and never through a link (O_NOFOLLOW), and takes the old mode on its own
|
|
645
|
+
* descriptor, since the umask would otherwise set the new file's mode. Returns whether the
|
|
646
|
+
* file was replaced.
|
|
647
|
+
*
|
|
648
|
+
* @param {string} filePath
|
|
649
|
+
* @param {string} text
|
|
650
|
+
* @param {number} mode the permission bits of the file it replaces
|
|
651
|
+
* @returns {boolean}
|
|
652
|
+
*/
|
|
653
|
+
function replaceTextFile(filePath, text, mode) {
|
|
654
|
+
const c = fs.constants;
|
|
655
|
+
let probe;
|
|
656
|
+
try {
|
|
657
|
+
probe = fs.openSync(filePath, c.O_WRONLY | c.O_APPEND | (c.O_NOFOLLOW || 0) | (c.O_NONBLOCK || 0));
|
|
658
|
+
if (!fs.fstatSync(probe).isFile()) return false;
|
|
659
|
+
} catch (_) {
|
|
660
|
+
return false;
|
|
661
|
+
} finally {
|
|
662
|
+
if (probe !== undefined) {
|
|
663
|
+
try { fs.closeSync(probe); } catch (_) { /* the probe wrote nothing */ }
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
const temp = path.join(path.dirname(filePath), '.' + path.basename(filePath) + '.claim-' + process.pid);
|
|
667
|
+
let fd;
|
|
668
|
+
try {
|
|
669
|
+
fd = fs.openSync(temp, c.O_WRONLY | c.O_CREAT | c.O_EXCL | (c.O_NOFOLLOW || 0), 0o600);
|
|
670
|
+
} catch (_) {
|
|
671
|
+
return false;
|
|
672
|
+
}
|
|
673
|
+
try {
|
|
674
|
+
fs.fchmodSync(fd, mode);
|
|
675
|
+
fs.writeFileSync(fd, text, 'utf8');
|
|
676
|
+
fs.closeSync(fd);
|
|
677
|
+
fd = undefined;
|
|
678
|
+
fs.renameSync(temp, filePath);
|
|
679
|
+
return true;
|
|
680
|
+
} catch (_) {
|
|
681
|
+
if (fd !== undefined) {
|
|
682
|
+
try { fs.closeSync(fd); } catch (_) { /* the write already failed */ }
|
|
683
|
+
}
|
|
684
|
+
try { fs.unlinkSync(temp); } catch (_) { /* the temporary file is already gone */ }
|
|
685
|
+
return false;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
519
689
|
/**
|
|
520
690
|
* @param {string} dir
|
|
521
691
|
* @returns {boolean}
|
|
@@ -753,13 +923,18 @@ function trackedState(io, file) {
|
|
|
753
923
|
* the write failure (exit 3): no file is written. An `--evidence` file that is not
|
|
754
924
|
* clearly untracked is dropped, so the run goes on exactly as if `--evidence` was not
|
|
755
925
|
* given; the stderr summary then carries the closed note `evidence-ignored`. Stdout
|
|
756
|
-
* keeps its grammar.
|
|
926
|
+
* keeps its grammar. A `claim` run has its own refusal line for each of these cases
|
|
927
|
+
* (claimRefusalReason).
|
|
757
928
|
*
|
|
758
929
|
* @param {Io} io
|
|
759
930
|
* @param {ParsedArgs} args
|
|
760
931
|
* @returns {{ args: ParsedArgs } | { outcome: Outcome }}
|
|
761
932
|
*/
|
|
762
933
|
function admitPaths(io, args) {
|
|
934
|
+
if (args.kind === 'claim') {
|
|
935
|
+
const reason = claimRefusalReason(io, args);
|
|
936
|
+
return reason === null ? { args } : { outcome: claimRefusal(reason) };
|
|
937
|
+
}
|
|
763
938
|
if (args.kind !== 'verify' && args.kind !== 'publish' && args.kind !== 'readiness') return { args };
|
|
764
939
|
const outs = args.kind === 'verify' ? [args.blockOut, args.commentOut, args.staleOut]
|
|
765
940
|
: args.kind === 'readiness' ? [args.out] : [];
|
|
@@ -1424,7 +1599,16 @@ function runsAt(io, memo, sha) {
|
|
|
1424
1599
|
*/
|
|
1425
1600
|
function loadEvidence(file) {
|
|
1426
1601
|
const text = readTextFile(file);
|
|
1427
|
-
|
|
1602
|
+
return text === null ? null : parseEvidenceText(text);
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
/**
|
|
1606
|
+
* The sections of an evidence file's text, parsed (see loadEvidence).
|
|
1607
|
+
*
|
|
1608
|
+
* @param {string} text
|
|
1609
|
+
* @returns {EvidenceInput | null} null when unusable
|
|
1610
|
+
*/
|
|
1611
|
+
function parseEvidenceText(text) {
|
|
1428
1612
|
const sections = PE.evidenceSections(text);
|
|
1429
1613
|
if (!sections.ok) return null;
|
|
1430
1614
|
let plan = null;
|
|
@@ -2026,6 +2210,21 @@ const COMPLIANCE_UNREADABLE_LINE = 'TRACEABILITY: DEGRADED (compliance state unr
|
|
|
2026
2210
|
/** Every line a `publish` that did not run may print. The boundary admits each one with exit 2 only. */
|
|
2027
2211
|
const PUBLISH_REFUSAL_LINES = Object.freeze([EVIDENCE_UNAVAILABLE_LINE, COMPLIANCE_OFF_LINE, COMPLIANCE_UNREADABLE_LINE]);
|
|
2028
2212
|
|
|
2213
|
+
/** The answer of every `publish` that could not give its block: the one line, exit 2. */
|
|
2214
|
+
const PUBLISH_UNAVAILABLE = Object.freeze({ code: EXIT_CODES.INPUT_UNUSABLE, stdout: EVIDENCE_UNAVAILABLE_LINE + '\n' });
|
|
2215
|
+
|
|
2216
|
+
/**
|
|
2217
|
+
* Whether argv asks for `publish`. The boundary reads it after any failure, a usage
|
|
2218
|
+
* error and a throw included, so settleOutcome can answer a failed `publish` with
|
|
2219
|
+
* PUBLISH_UNAVAILABLE.
|
|
2220
|
+
*
|
|
2221
|
+
* @param {readonly string[]} argv process.argv
|
|
2222
|
+
* @returns {boolean}
|
|
2223
|
+
*/
|
|
2224
|
+
function isPublishRun(argv) {
|
|
2225
|
+
return Array.isArray(argv) && argv[2] === 'publish';
|
|
2226
|
+
}
|
|
2227
|
+
|
|
2029
2228
|
/**
|
|
2030
2229
|
* @typedef {{ status: string, reason: string | null }} Part
|
|
2031
2230
|
* One half of the `publish` answer: a closed status, or `DEGRADED` with its reason.
|
|
@@ -2077,8 +2276,26 @@ function traceReasons(body, comment) {
|
|
|
2077
2276
|
}
|
|
2078
2277
|
|
|
2079
2278
|
/**
|
|
2080
|
-
* The
|
|
2081
|
-
*
|
|
2279
|
+
* The test-plan tally that both scripted blocks print: `<verified>/<total>`, the given
|
|
2280
|
+
* word, then the two counts that make up `verified`. The readiness block prints it with
|
|
2281
|
+
* no word and the publish block with ` verified`.
|
|
2282
|
+
*
|
|
2283
|
+
* @param {{ total: number, counts: Readonly<Record<string, number>> }} fields the parsed EVIDENCE line
|
|
2284
|
+
* @param {'' | ' verified'} word
|
|
2285
|
+
* @returns {string}
|
|
2286
|
+
*/
|
|
2287
|
+
function planTally(fields, word) {
|
|
2288
|
+
const verifiedCi = fields.counts['VERIFIED-CI'];
|
|
2289
|
+
const attestedLocal = fields.counts['ATTESTED-LOCAL'];
|
|
2290
|
+
return (verifiedCi + attestedLocal) + '/' + fields.total + word
|
|
2291
|
+
+ ' (VERIFIED-CI ' + verifiedCi + ', ATTESTED-LOCAL ' + attestedLocal + ')';
|
|
2292
|
+
}
|
|
2293
|
+
|
|
2294
|
+
/**
|
|
2295
|
+
* The `## PR Evidence` block of the update-pr-evidence contract: the EVIDENCE line, the
|
|
2296
|
+
* `Test plan:` line read from it, the Body and Comment line, then its TRACEABILITY lines.
|
|
2297
|
+
* The caller holds the precondition that `evidenceLine` parses; a line that does not is a
|
|
2298
|
+
* broken invariant, which the catch in main turns into "evidence unavailable".
|
|
2082
2299
|
*
|
|
2083
2300
|
* @param {string} evidenceLine
|
|
2084
2301
|
* @param {Part} body
|
|
@@ -2086,9 +2303,12 @@ function traceReasons(body, comment) {
|
|
|
2086
2303
|
* @returns {string}
|
|
2087
2304
|
*/
|
|
2088
2305
|
function renderPublishBlock(evidenceLine, body, comment) {
|
|
2306
|
+
const evidence = PE.parseEvidenceLine(evidenceLine);
|
|
2307
|
+
if (!evidence.ok) throw new Error('verify-evidence: unchecked evidence line');
|
|
2089
2308
|
const lines = [
|
|
2090
2309
|
'## PR Evidence',
|
|
2091
2310
|
evidenceLine,
|
|
2311
|
+
'Test plan: ' + planTally(evidence.value, ' verified'),
|
|
2092
2312
|
'**Body**: ' + partText(body) + ' · **Comment**: ' + partText(comment),
|
|
2093
2313
|
...traceReasons(body, comment).map(r => 'TRACEABILITY: DEGRADED (' + r + ')'),
|
|
2094
2314
|
];
|
|
@@ -2110,8 +2330,8 @@ function parsePart(text, statuses) {
|
|
|
2110
2330
|
|
|
2111
2331
|
/**
|
|
2112
2332
|
* D-OP-BLOCK-GATE: read a `publish` block back into its parts, or null when it is not
|
|
2113
|
-
* exactly what renderPublishBlock prints
|
|
2114
|
-
* accepts it.
|
|
2333
|
+
* exactly what renderPublishBlock prints, so a `Test plan:` line that disagrees with the
|
|
2334
|
+
* EVIDENCE line is refused. The boundary prints a block only when this accepts it.
|
|
2115
2335
|
*
|
|
2116
2336
|
* @param {string} text the whole stdout, its final newline included
|
|
2117
2337
|
* @returns {{ evidenceLine: string, body: Part, comment: Part } | null}
|
|
@@ -2119,9 +2339,9 @@ function parsePart(text, statuses) {
|
|
|
2119
2339
|
function parsePublishBlock(text) {
|
|
2120
2340
|
if (typeof text !== 'string' || !text.endsWith('\n') || text.length > 8192) return null;
|
|
2121
2341
|
const lines = text.slice(0, -1).split('\n');
|
|
2122
|
-
if (lines.length <
|
|
2342
|
+
if (lines.length < 4 || lines.length > 6 || lines[0] !== '## PR Evidence') return null;
|
|
2123
2343
|
if (!PE.parseEvidenceLine(lines[1]).ok) return null;
|
|
2124
|
-
const m = PUBLISH_PARTS_RE.exec(lines[
|
|
2344
|
+
const m = PUBLISH_PARTS_RE.exec(lines[3]);
|
|
2125
2345
|
if (m === null || m.groups === undefined) return null;
|
|
2126
2346
|
const body = parsePart(m.groups.body, BODY_STATUSES);
|
|
2127
2347
|
const comment = parsePart(m.groups.comment, COMMENT_STATUSES);
|
|
@@ -2138,7 +2358,7 @@ function parsePublishBlock(text) {
|
|
|
2138
2358
|
*/
|
|
2139
2359
|
function evidenceUnavailable(io, reason) {
|
|
2140
2360
|
io.stderr('verify-evidence: publish: ' + reason + '\n');
|
|
2141
|
-
return
|
|
2361
|
+
return PUBLISH_UNAVAILABLE;
|
|
2142
2362
|
}
|
|
2143
2363
|
|
|
2144
2364
|
/**
|
|
@@ -2826,9 +3046,7 @@ function readinessPass(io, args, deps) {
|
|
|
2826
3046
|
: String(threads.value.unresolved),
|
|
2827
3047
|
decision: decisionText,
|
|
2828
3048
|
ci: ci.status,
|
|
2829
|
-
plan:
|
|
2830
|
-
: (evidence.verifiedCi + evidence.attestedLocal) + '/' + evidence.total
|
|
2831
|
-
+ ' (VERIFIED-CI ' + evidence.verifiedCi + ', ATTESTED-LOCAL ' + evidence.attestedLocal + ')',
|
|
3049
|
+
plan: fields === null ? 'unavailable' : planTally(fields, ''),
|
|
2832
3050
|
approval: approvalText(args.requireNonAuthor, evidence),
|
|
2833
3051
|
};
|
|
2834
3052
|
return { outcome: { code: EXIT_CODES.OK, stdout: renderReadinessBlock(block) }, head };
|
|
@@ -2922,6 +3140,289 @@ function runReadiness(io, args, deps) {
|
|
|
2922
3140
|
return args.out === null ? pass.outcome : writeReadinessOut(io, args.out, args.pr, pass.outcome, pass.head, planSource(args));
|
|
2923
3141
|
}
|
|
2924
3142
|
|
|
3143
|
+
// ---------------------------------------------------------------------------
|
|
3144
|
+
// claim — D-CLAIM-APPEND
|
|
3145
|
+
// ---------------------------------------------------------------------------
|
|
3146
|
+
//
|
|
3147
|
+
// A Test or Validate report becomes claim lines in the evidence file. The caller passes
|
|
3148
|
+
// only the facts of its report (the HEAD, the gate, one outcome per TP). This script owns
|
|
3149
|
+
// the rest: the line shapes (the claim parser of pr-evidence.cjs accepts each one before
|
|
3150
|
+
// it is written), the HEAD rule, the exit range, the `## Claims` section and the append.
|
|
3151
|
+
// A claim is an input to `verify`, never a state: only pr-evidence.cjs and `verify`
|
|
3152
|
+
// assign states.
|
|
3153
|
+
|
|
3154
|
+
/**
|
|
3155
|
+
* @param {string} reason one of the CLAIM_REFUSAL_RE reasons
|
|
3156
|
+
* @returns {Outcome}
|
|
3157
|
+
*/
|
|
3158
|
+
function claimRefusal(reason) {
|
|
3159
|
+
return { code: EXIT_CODES.INPUT_UNUSABLE, stdout: 'CLAIM refused (' + reason + ')\n' };
|
|
3160
|
+
}
|
|
3161
|
+
|
|
3162
|
+
/** The most folders one path can hold: the bound of the walk up to the nearest folder that exists. */
|
|
3163
|
+
const FOLDER_DEPTH_MAX = 4096;
|
|
3164
|
+
|
|
3165
|
+
/**
|
|
3166
|
+
* Whether `child` lies below `parent`, by name alone. `parent` itself, and a path outside
|
|
3167
|
+
* it, are not below it.
|
|
3168
|
+
*
|
|
3169
|
+
* @param {string} parent absolute
|
|
3170
|
+
* @param {string} child absolute
|
|
3171
|
+
* @returns {boolean}
|
|
3172
|
+
*/
|
|
3173
|
+
function isBelow(parent, child) {
|
|
3174
|
+
const rel = path.relative(parent, child);
|
|
3175
|
+
return rel !== '' && rel !== '..' && !rel.startsWith('..' + path.sep) && !path.isAbsolute(rel);
|
|
3176
|
+
}
|
|
3177
|
+
|
|
3178
|
+
/**
|
|
3179
|
+
* D-CLAIM-FOLDER: the folder of the evidence file exists, or this run makes it. The Write
|
|
3180
|
+
* tool made the folders of a file in 3.11.0, so a first claim into a folder that is not
|
|
3181
|
+
* there yet worked. A run makes a folder only inside the working directory: the folder lies
|
|
3182
|
+
* below `io.cwd` by name, and the nearest folder that already exists resolves to `io.cwd` or
|
|
3183
|
+
* below it, so a link out of the working directory makes nothing. Each folder it makes goes
|
|
3184
|
+
* to `io.made`, deepest first, for removeMadeFolders. A path that holds a file where a folder
|
|
3185
|
+
* must be, or that the run may not make, is unusable. After each folder is made, its real path
|
|
3186
|
+
* must lie below the real working directory; if a link swapped in after the first check led the
|
|
3187
|
+
* folder out, the run is unusable and removeMadeFolders takes the folder away again. This check
|
|
3188
|
+
* narrows the window for such a swap; it does not close it, because the later open of the
|
|
3189
|
+
* evidence file resolves the path again.
|
|
3190
|
+
*
|
|
3191
|
+
* @param {Io} io
|
|
3192
|
+
* @param {string} dir absolute
|
|
3193
|
+
* @returns {boolean} whether the folder exists now
|
|
3194
|
+
*/
|
|
3195
|
+
function ensureClaimFolder(io, dir) {
|
|
3196
|
+
if (isRealDirectory(dir)) return true;
|
|
3197
|
+
try {
|
|
3198
|
+
const root = path.resolve(io.cwd);
|
|
3199
|
+
if (fs.lstatSync(dir, { throwIfNoEntry: false }) !== undefined || !isBelow(root, dir)) return false;
|
|
3200
|
+
let near = dir;
|
|
3201
|
+
for (let i = 0; i < FOLDER_DEPTH_MAX && fs.lstatSync(near, { throwIfNoEntry: false }) === undefined; i++) {
|
|
3202
|
+
near = path.dirname(near);
|
|
3203
|
+
}
|
|
3204
|
+
const real = fs.realpathSync(near);
|
|
3205
|
+
const realRoot = fs.realpathSync(root);
|
|
3206
|
+
if (real !== realRoot && !isBelow(realRoot, real)) return false;
|
|
3207
|
+
let at = near;
|
|
3208
|
+
for (const part of path.relative(near, dir).split(path.sep)) {
|
|
3209
|
+
at = path.join(at, part);
|
|
3210
|
+
fs.mkdirSync(at);
|
|
3211
|
+
io.made.unshift(at);
|
|
3212
|
+
if (!isBelow(realRoot, fs.realpathSync(at))) return false;
|
|
3213
|
+
}
|
|
3214
|
+
return true;
|
|
3215
|
+
} catch (_) {
|
|
3216
|
+
return false;
|
|
3217
|
+
}
|
|
3218
|
+
}
|
|
3219
|
+
|
|
3220
|
+
/**
|
|
3221
|
+
* Take away the folders a `claim` run made. Each goes through rmdir, which refuses a folder
|
|
3222
|
+
* that holds anything, so a folder that holds the evidence file, or a file another process
|
|
3223
|
+
* put there, stays. It is never a recursive delete.
|
|
3224
|
+
*
|
|
3225
|
+
* @param {Io} io
|
|
3226
|
+
* @returns {void}
|
|
3227
|
+
*/
|
|
3228
|
+
function removeMadeFolders(io) {
|
|
3229
|
+
for (const dir of io.made.splice(0)) {
|
|
3230
|
+
try { fs.rmdirSync(dir); } catch (_) { /* a folder that holds a file stays */ }
|
|
3231
|
+
}
|
|
3232
|
+
}
|
|
3233
|
+
|
|
3234
|
+
/**
|
|
3235
|
+
* The reason a `claim` run must not start, or null. Nothing is read or written before
|
|
3236
|
+
* this, but the folder of the evidence file, when it is missing: the HEAD is one 40-hex
|
|
3237
|
+
* SHA, each path is a path the commands build, the evidence file sits in a folder that
|
|
3238
|
+
* exists or that the run makes inside the working directory (D-CLAIM-FOLDER), and git says
|
|
3239
|
+
* clearly that neither file is tracked (D-TRACKED-RESULT).
|
|
3240
|
+
*
|
|
3241
|
+
* @param {Io} io
|
|
3242
|
+
* @param {Extract<ParsedArgs, { kind: 'claim' }>} args
|
|
3243
|
+
* @returns {string | null}
|
|
3244
|
+
*/
|
|
3245
|
+
function claimRefusalReason(io, args) {
|
|
3246
|
+
if (!SHA40_RE.test(args.head)) return 'head not one sha';
|
|
3247
|
+
if (!EVIDENCE_PATH_RE.test(args.evidence) || !ensureClaimFolder(io, path.dirname(path.resolve(args.evidence)))) return 'evidence file unusable';
|
|
3248
|
+
if (args.plan !== null && !EVIDENCE_PATH_RE.test(args.plan)) return 'plan file unusable';
|
|
3249
|
+
if (trackedState(io, args.evidence) !== 'untracked') return 'evidence file tracked';
|
|
3250
|
+
if (args.plan !== null && trackedState(io, args.plan) !== 'untracked') return 'plan file unusable';
|
|
3251
|
+
return null;
|
|
3252
|
+
}
|
|
3253
|
+
|
|
3254
|
+
/**
|
|
3255
|
+
* Whether one line is exactly one claim to the claim parser of pr-evidence.cjs.
|
|
3256
|
+
*
|
|
3257
|
+
* @param {string} line
|
|
3258
|
+
* @returns {boolean}
|
|
3259
|
+
*/
|
|
3260
|
+
function claimLineOk(line) {
|
|
3261
|
+
const r = PE.parseClaims(line);
|
|
3262
|
+
if (!r.ok) return false;
|
|
3263
|
+
const gates = r.value.gates;
|
|
3264
|
+
return r.value.malformed === 0 && r.value.tp.size + (gates.validate === null ? 0 : 1) + (gates.qa === null ? 0 : 1) === 1;
|
|
3265
|
+
}
|
|
3266
|
+
|
|
3267
|
+
/**
|
|
3268
|
+
* Assert a line before it is written: one claim to the claim parser. Every caller builds
|
|
3269
|
+
* the line from parts that are already checked; this is that precondition held at the sink.
|
|
3270
|
+
*
|
|
3271
|
+
* @param {string} line
|
|
3272
|
+
* @returns {string}
|
|
3273
|
+
*/
|
|
3274
|
+
function claimLine(line) {
|
|
3275
|
+
if (!claimLineOk(line)) throw new Error('verify-evidence: unchecked claim line');
|
|
3276
|
+
return line;
|
|
3277
|
+
}
|
|
3278
|
+
|
|
3279
|
+
/**
|
|
3280
|
+
* The text to append: the lines, behind the `## Claims` heading when the file has no such
|
|
3281
|
+
* section (a blank line first, when the file holds anything). The caller appends only when
|
|
3282
|
+
* the section is the last of the file, so the lines go at its end.
|
|
3283
|
+
*
|
|
3284
|
+
* @param {string} text the evidence file as it is, '' when it is absent
|
|
3285
|
+
* @param {boolean} hasSection
|
|
3286
|
+
* @param {readonly string[]} lines
|
|
3287
|
+
* @returns {string}
|
|
3288
|
+
*/
|
|
3289
|
+
function claimAppendText(text, hasSection, lines) {
|
|
3290
|
+
const body = lines.join('\n') + '\n';
|
|
3291
|
+
if (text === '') return PE.CLAIMS_HEADING + '\n' + body;
|
|
3292
|
+
const lead = text.endsWith('\n') ? '' : '\n';
|
|
3293
|
+
return hasSection ? lead + body : lead + '\n' + PE.CLAIMS_HEADING + '\n' + body;
|
|
3294
|
+
}
|
|
3295
|
+
|
|
3296
|
+
/**
|
|
3297
|
+
* D-CLAIM-INSERT: the whole text with the lines put at the end of a `## Claims` section that
|
|
3298
|
+
* another section follows: behind the last row of the section that is not blank, before the
|
|
3299
|
+
* blank rows that precede the next heading. A row is blank as the parser of pr-evidence.cjs
|
|
3300
|
+
* reads it, an empty row once its CR is gone. A text with no such section comes back as it was.
|
|
3301
|
+
*
|
|
3302
|
+
* @param {string} text
|
|
3303
|
+
* @param {readonly string[]} lines
|
|
3304
|
+
* @returns {string}
|
|
3305
|
+
*/
|
|
3306
|
+
function claimInsertText(text, lines) {
|
|
3307
|
+
const rows = text.split('\n');
|
|
3308
|
+
/** @param {string} row @returns {string} */
|
|
3309
|
+
const bare = row => row.replace(/\r$/, '');
|
|
3310
|
+
const start = rows.findIndex(row => bare(row) === PE.CLAIMS_HEADING);
|
|
3311
|
+
if (start === -1) return text;
|
|
3312
|
+
let end = rows.findIndex((row, i) => i > start && (bare(row) === '##' || bare(row).startsWith('## ')));
|
|
3313
|
+
if (end === -1) end = rows.length;
|
|
3314
|
+
let last = end - 1;
|
|
3315
|
+
while (last > start && bare(rows[last]) === '') last--;
|
|
3316
|
+
rows.splice(last + 1, 0, ...lines);
|
|
3317
|
+
return rows.join('\n');
|
|
3318
|
+
}
|
|
3319
|
+
|
|
3320
|
+
/**
|
|
3321
|
+
* Whether `inserted` is `text` with the lines added at the end of its `## Claims` section and
|
|
3322
|
+
* nothing else changed, as the parser of pr-evidence.cjs reads both: the Claims section grew by
|
|
3323
|
+
* exactly the lines, its other sections are the same, and the result parses as an evidence file.
|
|
3324
|
+
* This is the check the replace is held to before it writes.
|
|
3325
|
+
*
|
|
3326
|
+
* @param {string} text
|
|
3327
|
+
* @param {string} inserted
|
|
3328
|
+
* @param {readonly string[]} lines
|
|
3329
|
+
* @returns {boolean}
|
|
3330
|
+
*/
|
|
3331
|
+
function claimInserted(text, inserted, lines) {
|
|
3332
|
+
const before = PE.evidenceSections(text);
|
|
3333
|
+
const after = PE.evidenceSections(inserted);
|
|
3334
|
+
if (!before.ok || !after.ok || before.value.claims === null) return false;
|
|
3335
|
+
return after.value.claims === before.value.claims + '\n' + lines.join('\n')
|
|
3336
|
+
&& after.value.testPlan === before.value.testPlan
|
|
3337
|
+
&& after.value.exceptions === before.value.exceptions
|
|
3338
|
+
&& parseEvidenceText(inserted) !== null;
|
|
3339
|
+
}
|
|
3340
|
+
|
|
3341
|
+
/**
|
|
3342
|
+
* The TP numbers the plan lists: the `## Test Plan` of the `--plan` file, else of the
|
|
3343
|
+
* evidence file. A plan with no TP line, or no plan section, lists none. Null when the
|
|
3344
|
+
* `--plan` file cannot serve as a plan.
|
|
3345
|
+
*
|
|
3346
|
+
* @param {Extract<ParsedArgs, { kind: 'claim' }>} args
|
|
3347
|
+
* @param {EvidenceInput} evidence
|
|
3348
|
+
* @returns {Set<number> | null}
|
|
3349
|
+
*/
|
|
3350
|
+
function claimPlanIds(args, evidence) {
|
|
3351
|
+
let plan = evidence.plan;
|
|
3352
|
+
if (args.plan !== null) {
|
|
3353
|
+
const text = readTextFile(args.plan);
|
|
3354
|
+
const parsed = text === null ? null : PE.parsePlan(sectionOrWhole(text, 'testPlan'));
|
|
3355
|
+
if (parsed === null || (!parsed.ok && parsed.error.code !== 'empty')) return null;
|
|
3356
|
+
plan = parsed.ok ? parsed.value : { tps: [] };
|
|
3357
|
+
}
|
|
3358
|
+
return new Set((plan === null ? [] : plan.tps).map(tp => tp.id));
|
|
3359
|
+
}
|
|
3360
|
+
|
|
3361
|
+
/**
|
|
3362
|
+
* D-CLAIM-APPEND: `claim`. The checks of claimRefusalReason have passed (admitPaths).
|
|
3363
|
+
* The evidence file is read as `verify` reads it; a file that does not parse is refused, as
|
|
3364
|
+
* `verify` refuses it. Then every line is built and checked, and one write puts them all in.
|
|
3365
|
+
* That write is an append when the `## Claims` section is the last section or is absent. When
|
|
3366
|
+
* another section follows it, the write is a whole-file replace (D-CLAIM-INSERT) that adds the
|
|
3367
|
+
* lines at the end of the Claims section, held to claimInserted before it writes. One claim
|
|
3368
|
+
* run writes at a time, as the commands run it. A run that writes no line (every TP off the
|
|
3369
|
+
* plan) changes no file.
|
|
3370
|
+
*
|
|
3371
|
+
* @param {Io} io
|
|
3372
|
+
* @param {Extract<ParsedArgs, { kind: 'claim' }>} args
|
|
3373
|
+
* @returns {Outcome}
|
|
3374
|
+
*/
|
|
3375
|
+
function runClaim(io, args) {
|
|
3376
|
+
const file = path.resolve(args.evidence);
|
|
3377
|
+
/** @type {fs.Stats | undefined} */
|
|
3378
|
+
let st;
|
|
3379
|
+
try {
|
|
3380
|
+
st = fs.lstatSync(file, { throwIfNoEntry: false });
|
|
3381
|
+
} catch (_) {
|
|
3382
|
+
return claimRefusal('evidence file unusable');
|
|
3383
|
+
}
|
|
3384
|
+
if (st !== undefined && !st.isFile()) return claimRefusal('evidence file unusable');
|
|
3385
|
+
const text = st === undefined ? '' : readTextFile(file);
|
|
3386
|
+
const evidence = text === null ? null : parseEvidenceText(text);
|
|
3387
|
+
if (text === null || evidence === null) return claimRefusal('evidence file unusable');
|
|
3388
|
+
const headings = text.split('\n').map(row => row.replace(/\r$/, '')).filter(row => row === '##' || row.startsWith('## '));
|
|
3389
|
+
const hasSection = headings.includes(PE.CLAIMS_HEADING);
|
|
3390
|
+
const sectionFollowed = hasSection && headings[headings.length - 1] !== PE.CLAIMS_HEADING;
|
|
3391
|
+
const listed = args.gate ? new Set() : claimPlanIds(args, evidence);
|
|
3392
|
+
if (listed === null) return claimRefusal('plan file unusable');
|
|
3393
|
+
|
|
3394
|
+
/** @type {string[]} */
|
|
3395
|
+
const lines = [];
|
|
3396
|
+
const tally = { PASS: 0, FAIL: 0, SKIP: 0 };
|
|
3397
|
+
if (args.gate) {
|
|
3398
|
+
lines.push(claimLine('- gate:validate PASS sha:' + args.head + ' by:validate'));
|
|
3399
|
+
tally.PASS++;
|
|
3400
|
+
}
|
|
3401
|
+
for (const tp of args.tps) {
|
|
3402
|
+
if (!listed.has(tp.id)) continue;
|
|
3403
|
+
const bare = '- TP-' + tp.id + ' ' + tp.outcome + ' sha:' + args.head + ' by:test';
|
|
3404
|
+
const withExit = tp.exit === null ? bare : bare + ' exit:' + tp.exit;
|
|
3405
|
+
lines.push(claimLine(claimLineOk(withExit) ? withExit : bare));
|
|
3406
|
+
tally[tp.outcome]++;
|
|
3407
|
+
}
|
|
3408
|
+
if (lines.length > 0) {
|
|
3409
|
+
let written;
|
|
3410
|
+
if (sectionFollowed) {
|
|
3411
|
+
const inserted = claimInsertText(text, lines);
|
|
3412
|
+
if (!claimInserted(text, inserted, lines)) return claimRefusal('evidence file unusable');
|
|
3413
|
+
written = replaceTextFile(file, inserted, st === undefined ? 0o600 : st.mode & 0o777);
|
|
3414
|
+
} else {
|
|
3415
|
+
written = appendTextFile(file, claimAppendText(text, hasSection, lines));
|
|
3416
|
+
}
|
|
3417
|
+
if (!written) return refuse(io, EXIT_CODES.WRITE_FAILED, 'an output file could not be written');
|
|
3418
|
+
}
|
|
3419
|
+
const skipped = args.gate ? 0 : args.tps.length - lines.length;
|
|
3420
|
+
return {
|
|
3421
|
+
code: EXIT_CODES.OK,
|
|
3422
|
+
stdout: 'CLAIM appended:' + lines.length + ' PASS:' + tally.PASS + ' FAIL:' + tally.FAIL + ' SKIP:' + tally.SKIP + ' skipped:' + skipped + '\n',
|
|
3423
|
+
};
|
|
3424
|
+
}
|
|
3425
|
+
|
|
2925
3426
|
// ---------------------------------------------------------------------------
|
|
2926
3427
|
// main — returns {code, stdout}; never calls process.exit
|
|
2927
3428
|
// ---------------------------------------------------------------------------
|
|
@@ -2984,6 +3485,7 @@ function main(argv, deps) {
|
|
|
2984
3485
|
deadline: now() + DEADLINE_MS,
|
|
2985
3486
|
budget: { ghCalls: 0, runViews: 0, throttled: false },
|
|
2986
3487
|
notes: new Set(),
|
|
3488
|
+
made: [],
|
|
2987
3489
|
stderr: typeof d.stderr === 'function' ? d.stderr : (text => { process.stderr.write(text); }),
|
|
2988
3490
|
};
|
|
2989
3491
|
const parsed = parseArgs(argv);
|
|
@@ -3005,6 +3507,7 @@ function main(argv, deps) {
|
|
|
3005
3507
|
else if (args.kind === 'splice') outcome = runSplice(io, args);
|
|
3006
3508
|
else if (args.kind === 'publish') outcome = runPublish(io, args, d);
|
|
3007
3509
|
else if (args.kind === 'readiness') outcome = runReadiness(io, args, d);
|
|
3510
|
+
else if (args.kind === 'claim') outcome = runClaim(io, args);
|
|
3008
3511
|
else outcome = runReadback(io, args);
|
|
3009
3512
|
}
|
|
3010
3513
|
} catch (err) {
|
|
@@ -3019,6 +3522,7 @@ function main(argv, deps) {
|
|
|
3019
3522
|
if (args.out !== null) outcome = writeReadinessOut(io, args.out, args.pr, outcome, null, planSource(args));
|
|
3020
3523
|
} else outcome = refuse(io, EXIT_CODES.REMOTE_FAILURE, label);
|
|
3021
3524
|
}
|
|
3525
|
+
removeMadeFolders(io);
|
|
3022
3526
|
if (io.notes.size > 0) io.stderr('verify-evidence: notes: ' + [...io.notes].sort().join(',') + '\n');
|
|
3023
3527
|
return outcome;
|
|
3024
3528
|
}
|
|
@@ -3038,12 +3542,26 @@ function errorLabel(err) {
|
|
|
3038
3542
|
/**
|
|
3039
3543
|
* D-VERIFY-STDOUT: settle whatever main() returned into what the boundary may
|
|
3040
3544
|
* print. The code must be a known exit code and stdout must be empty or one of
|
|
3041
|
-
* the closed shapes for that code; anything else prints nothing and exits 4.
|
|
3545
|
+
* the closed shapes for that code; anything else prints nothing and exits 4. A
|
|
3546
|
+
* `publish` run is the exception: it never settles to empty stdout, so what would
|
|
3547
|
+
* print nothing prints the evidence unavailable line with exit 2.
|
|
3042
3548
|
*
|
|
3043
3549
|
* @param {unknown} outcome
|
|
3550
|
+
* @param {boolean} [publish] true when the run is a `publish`
|
|
3044
3551
|
* @returns {Outcome}
|
|
3045
3552
|
*/
|
|
3046
|
-
function settleOutcome(outcome) {
|
|
3553
|
+
function settleOutcome(outcome, publish) {
|
|
3554
|
+
const settled = settleShape(outcome);
|
|
3555
|
+
return publish === true && settled.stdout === '' ? PUBLISH_UNAVAILABLE : settled;
|
|
3556
|
+
}
|
|
3557
|
+
|
|
3558
|
+
/**
|
|
3559
|
+
* The shape check of settleOutcome, for every subcommand.
|
|
3560
|
+
*
|
|
3561
|
+
* @param {unknown} outcome
|
|
3562
|
+
* @returns {Outcome}
|
|
3563
|
+
*/
|
|
3564
|
+
function settleShape(outcome) {
|
|
3047
3565
|
const o = /** @type {any} */ (outcome);
|
|
3048
3566
|
const failed = { code: EXIT_CODES.REMOTE_FAILURE, stdout: '' };
|
|
3049
3567
|
if (o === null || typeof o !== 'object' || typeof o.stdout !== 'string') return failed;
|
|
@@ -3073,6 +3591,14 @@ function settleOutcome(outcome) {
|
|
|
3073
3591
|
const passing = readback.groups.outcome === 'ok';
|
|
3074
3592
|
return o.code === (passing ? EXIT_CODES.OK : EXIT_CODES.OUTPUT_GATE_REFUSED) ? { code: o.code, stdout: o.stdout } : failed;
|
|
3075
3593
|
}
|
|
3594
|
+
const claimRefused = CLAIM_REFUSAL_RE.exec(text);
|
|
3595
|
+
if (claimRefused !== null) return o.code === EXIT_CODES.INPUT_UNUSABLE ? pass : failed;
|
|
3596
|
+
const claimed = CLAIM_APPENDED_RE.exec(text);
|
|
3597
|
+
if (claimed !== null && claimed.groups !== undefined) {
|
|
3598
|
+
const g = claimed.groups;
|
|
3599
|
+
const sums = Number(g.n) === Number(g.pass) + Number(g.fail) + Number(g.skip);
|
|
3600
|
+
return o.code === EXIT_CODES.OK && sums && Number(g.n) <= CAPS.TPS && Number(g.skipped) <= CAPS.TPS ? pass : failed;
|
|
3601
|
+
}
|
|
3076
3602
|
if (o.code !== EXIT_CODES.OK) return failed;
|
|
3077
3603
|
if (PE.parseEvidenceLine(text).ok) return { code: o.code, stdout: o.stdout };
|
|
3078
3604
|
const block = PE.parseBlock(text);
|
|
@@ -3094,7 +3620,7 @@ if (require.main === module) {
|
|
|
3094
3620
|
process.stderr.write('verify-evidence: internal error (' + errorLabel(err) + ')\n');
|
|
3095
3621
|
outcome = { code: EXIT_CODES.REMOTE_FAILURE, stdout: '' };
|
|
3096
3622
|
}
|
|
3097
|
-
const settled = settleOutcome(outcome);
|
|
3623
|
+
const settled = settleOutcome(outcome, isPublishRun(process.argv));
|
|
3098
3624
|
if (settled.stdout !== '') process.stdout.write(settled.stdout);
|
|
3099
3625
|
process.exitCode = settled.code;
|
|
3100
3626
|
}
|