devflow-kit 3.10.0 → 3.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +74 -0
- package/dist/agents/architect.md +87 -0
- package/dist/agents/code.md +21 -25
- package/dist/agents/debug.md +21 -49
- 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/backup-session.js +131 -0
- package/dist/cli/commands/compliance.js +16 -3
- package/dist/cli/commands/curation.js +12 -4
- package/dist/cli/commands/init.js +36 -55
- package/dist/cli/commands/install-report.js +95 -0
- package/dist/cli/commands/legacy-sweep.js +140 -45
- package/dist/cli/commands/rules.js +50 -13
- package/dist/cli/commands/uninstall.js +62 -24
- package/dist/commands/bug-analysis.md +31 -62
- package/dist/commands/code-review.md +38 -87
- package/dist/commands/debug.md +32 -82
- package/dist/commands/dynamic-build.md +155 -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 +98 -186
- package/dist/commands/plan.md +60 -107
- package/dist/commands/release.md +25 -91
- package/dist/commands/research.md +34 -103
- package/dist/commands/resolve.md +64 -128
- package/dist/commands/self-review.md +35 -99
- 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 +31 -62
- package/dist/compliance-off/commands/code-review.md +34 -82
- package/dist/compliance-off/commands/dynamic-build.md +149 -205
- package/dist/compliance-off/commands/dynamic-tickets.md +65 -186
- package/dist/compliance-off/commands/implement.md +94 -180
- package/dist/compliance-off/commands/plan.md +60 -107
- package/dist/compliance-off/commands/release.md +25 -90
- package/dist/compliance-off/commands/resolve.md +54 -128
- 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/compliance.js +14 -0
- package/dist/core/fs-atomic.js +5 -1
- package/dist/core/orphan-sweep.js +20 -2
- package/dist/core/path-set.js +15 -0
- package/dist/core/plugins.js +6 -5
- package/dist/core/reference-sweep.js +42 -25
- package/dist/core/retired-shipped-names.js +278 -0
- package/dist/curation-compliance-off/agents/triage.md +7 -13
- package/dist/curation-compliance-off/commands/bug-analysis.md +21 -41
- package/dist/curation-compliance-off/commands/code-review.md +25 -62
- package/dist/curation-compliance-off/commands/dynamic-build.md +142 -194
- package/dist/curation-compliance-off/commands/dynamic-tickets.md +54 -173
- package/dist/curation-compliance-off/commands/implement.md +86 -161
- package/dist/curation-compliance-off/commands/plan.md +51 -86
- package/dist/curation-compliance-off/commands/release.md +17 -78
- package/dist/curation-compliance-off/commands/resolve.md +45 -108
- package/dist/curation-off/agents/architect.md +81 -0
- package/dist/curation-off/agents/code.md +20 -23
- package/dist/curation-off/agents/debug.md +19 -47
- 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 +21 -41
- package/dist/curation-off/commands/code-review.md +29 -67
- package/dist/curation-off/commands/debug.md +24 -63
- package/dist/curation-off/commands/dynamic-build.md +148 -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 +90 -167
- package/dist/curation-off/commands/plan.md +51 -86
- package/dist/curation-off/commands/release.md +17 -79
- package/dist/curation-off/commands/research.md +23 -81
- package/dist/curation-off/commands/resolve.md +55 -108
- package/dist/curation-off/commands/self-review.md +26 -78
- 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/compliance-install.js +38 -13
- package/dist/targets/claude-code/compliance-off-files.js +5 -1
- package/dist/targets/claude-code/curation-install.js +77 -32
- package/dist/targets/claude-code/folder-swap.js +142 -0
- package/dist/targets/claude-code/install-backup.js +780 -0
- package/dist/targets/claude-code/installer.js +383 -192
- package/dist/targets/claude-code/legacy-skill-hashes.js +170 -0
- package/dist/targets/claude-code/linked-folder.js +5 -11
- package/dist/targets/claude-code/reference-converge.js +15 -2
- package/dist/targets/claude-code/rules-folder.js +34 -0
- package/dist/targets/claude-code/shipped-set.js +160 -0
- package/dist/targets/claude-code/workflow-folder-install.js +32 -12
- 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 +41 -0
- package/src/assets/agents/code.mds +18 -20
- package/src/assets/agents/curate.md +26 -35
- package/src/assets/agents/debug.mds +21 -49
- 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 +34 -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 +47 -51
- 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 +75 -117
- package/src/assets/commands/plan.mds +27 -57
- package/src/assets/commands/release.mds +4 -62
- package/src/assets/commands/research.mds +7 -53
- 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 +150 -18
- 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 +186 -0
- package/src/assets/scripts/resolve-settings.cjs +14 -9
- package/src/assets/scripts/verify-evidence.cjs +355 -22
- 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 +1 -35
- 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
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
// value. The level comes from these two keys only — never from a task, a diff or
|
|
64
64
|
// project.json.
|
|
65
65
|
//
|
|
66
|
-
// stdout is exactly one line plus "\n"
|
|
66
|
+
// stdout is exactly one line plus "\n" (D-SETTINGS-LINE):
|
|
67
67
|
// TRACKER=<github|jira|linear> TRACKER_SOURCE=<project|personal|machine|default>
|
|
68
68
|
// TRACKER_WARN=<none|mismatch|invalid> SITE=<url|none> KEY=<KEY|none>
|
|
69
69
|
// REVIEW_PUBLICATION=<off|auto|full> COMPLIANCE=<off|generic|id,…>
|
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
//
|
|
80
80
|
// Exit codes (a caller treats EVERY non-zero code as the fail-closed line):
|
|
81
81
|
// 0 resolved — the line above, including the whole-file rule's line
|
|
82
|
-
// 1 usage error —
|
|
82
|
+
// 1 usage error — usage on stderr; prints SETTINGS_FAIL_CLOSED_LINE
|
|
83
83
|
// 2 input unusable — <dir> missing or not a directory; prints
|
|
84
84
|
// SETTINGS_FAIL_CLOSED_LINE
|
|
85
85
|
// 3 never emitted — this script writes no file
|
|
@@ -192,10 +192,11 @@ const SETTINGS_LINE_RE = new RegExp(
|
|
|
192
192
|
);
|
|
193
193
|
|
|
194
194
|
/**
|
|
195
|
-
* The line every refusal prints (exits 2, 4 and 5), what a consumer uses in place
|
|
195
|
+
* The line every refusal prints (exits 1, 2, 4 and 5), what a consumer uses in place
|
|
196
196
|
* of any line it cannot accept, and the whole-file rule's line on a machine with
|
|
197
197
|
* no compliance lens. A constant, so the boundary can never fail
|
|
198
|
-
* to compose it.
|
|
198
|
+
* to compose it. The literal fallback of the _settings.mds partial is these bytes.
|
|
199
|
+
* Each field is the conservative reading for its consumer:
|
|
199
200
|
* TRACKER_WARN=invalid the Git agent degrades rather than guessing a provider
|
|
200
201
|
* REVIEW_PUBLICATION=off nothing is published (raised to a stub under required)
|
|
201
202
|
* COMPLIANCE=generic the review lens still runs
|
|
@@ -1278,13 +1279,12 @@ function settleOutcome(outcome) {
|
|
|
1278
1279
|
if (o === null || typeof o !== 'object' || typeof o.line !== 'string') {
|
|
1279
1280
|
return { code: EXIT_CODES.INTERNAL_ERROR, line: SETTINGS_FAIL_CLOSED_LINE };
|
|
1280
1281
|
}
|
|
1281
|
-
if (o.code === EXIT_CODES.USAGE) return { code: EXIT_CODES.USAGE, line: '' };
|
|
1282
1282
|
if (o.code === EXIT_CODES.RESOLVED) {
|
|
1283
1283
|
return isCoherentSettingsLine(o.line)
|
|
1284
1284
|
? { code: o.code, line: o.line }
|
|
1285
1285
|
: { code: EXIT_CODES.OUTPUT_GATE_REFUSED, line: SETTINGS_FAIL_CLOSED_LINE };
|
|
1286
1286
|
}
|
|
1287
|
-
if (o.code === EXIT_CODES.INPUT_UNUSABLE || o.code === EXIT_CODES.OUTPUT_GATE_REFUSED) {
|
|
1287
|
+
if (o.code === EXIT_CODES.USAGE || o.code === EXIT_CODES.INPUT_UNUSABLE || o.code === EXIT_CODES.OUTPUT_GATE_REFUSED) {
|
|
1288
1288
|
return { code: o.code, line: SETTINGS_FAIL_CLOSED_LINE };
|
|
1289
1289
|
}
|
|
1290
1290
|
return { code: EXIT_CODES.INTERNAL_ERROR, line: SETTINGS_FAIL_CLOSED_LINE };
|
|
@@ -1374,7 +1374,7 @@ function main(argv, deps) {
|
|
|
1374
1374
|
const args = parseArgs(argv);
|
|
1375
1375
|
if (args.kind === 'usage') {
|
|
1376
1376
|
process.stderr.write(args.usage + '\n');
|
|
1377
|
-
return { code: EXIT_CODES.USAGE, line:
|
|
1377
|
+
return { code: EXIT_CODES.USAGE, line: SETTINGS_FAIL_CLOSED_LINE };
|
|
1378
1378
|
}
|
|
1379
1379
|
if (!isUsableDirectory(args.dir)) {
|
|
1380
1380
|
process.stderr.write('resolve-settings: not a usable directory: '
|
|
@@ -1430,13 +1430,15 @@ if (require.main === module) {
|
|
|
1430
1430
|
outcome = { code: EXIT_CODES.INTERNAL_ERROR, line: SETTINGS_FAIL_CLOSED_LINE };
|
|
1431
1431
|
}
|
|
1432
1432
|
const settled = settleOutcome(outcome);
|
|
1433
|
-
|
|
1433
|
+
process.stdout.write(settled.line + '\n');
|
|
1434
1434
|
process.exitCode = settled.code;
|
|
1435
1435
|
}
|
|
1436
1436
|
|
|
1437
1437
|
// ---------------------------------------------------------------------------
|
|
1438
1438
|
// Exports — the CLI seam (src/core/evidence-policy.ts), the capture hooks' gate read
|
|
1439
|
-
// without jq (hooks/queue-append requires machineSwitchOn)
|
|
1439
|
+
// without jq (hooks/queue-append requires machineSwitchOn), the root resolver
|
|
1440
|
+
// (resolve-root.cjs takes the git half of its answer from gitToplevel, defaultExec and
|
|
1441
|
+
// ledgerRootOf) and the unit tests
|
|
1440
1442
|
// ---------------------------------------------------------------------------
|
|
1441
1443
|
|
|
1442
1444
|
module.exports = Object.freeze({
|
|
@@ -1451,10 +1453,13 @@ module.exports = Object.freeze({
|
|
|
1451
1453
|
foldSettings,
|
|
1452
1454
|
readRepoLayers,
|
|
1453
1455
|
machineSwitchOn,
|
|
1456
|
+
defaultExec,
|
|
1457
|
+
gitToplevel,
|
|
1454
1458
|
ledgerRootOf,
|
|
1455
1459
|
resolveSettings,
|
|
1456
1460
|
formatSettingsLine,
|
|
1457
1461
|
isCoherentSettingsLine,
|
|
1458
1462
|
serializeProjectSuggestion,
|
|
1463
|
+
settleOutcome,
|
|
1459
1464
|
main,
|
|
1460
1465
|
});
|
|
@@ -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,27 @@
|
|
|
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 the file's last section and is
|
|
72
|
+
// created when absent, and so is the file when its folder exists. The
|
|
73
|
+
// write is one append of every line, and the parser of pr-evidence.cjs
|
|
74
|
+
// accepts each line before it. It prints one line, exit 0:
|
|
75
|
+
// `CLAIM appended:<n> PASS:<n> FAIL:<n> SKIP:<n> skipped:<n>`
|
|
76
|
+
// (`skipped` counts the TPs that got no line), or exit 2 with
|
|
77
|
+
// `CLAIM refused (<reason>)`, the reason being `head not one sha`,
|
|
78
|
+
// `evidence file tracked` (git tracks it, or cannot say it does not),
|
|
79
|
+
// `evidence file unusable` or `plan file unusable`. A refusal writes nothing
|
|
58
80
|
// A git-tracked file counts as absent, like a tracked `.devflow/config.json`
|
|
59
81
|
// (D-TRACKED-RESULT). `verify`, `publish` and `readiness` ask git about an `--evidence`
|
|
60
82
|
// file before they read it, and about `--block-out`, `--comment-out`, `--stale-out` and
|
|
61
|
-
// `--out` before they write or remove one.
|
|
83
|
+
// `--out` before they write or remove one. `claim` asks about its `--evidence` and
|
|
84
|
+
// `--plan` files, and refuses both when either is not clearly untracked. Only a clear "not tracked" answer lets the
|
|
62
85
|
// file be read or written. A tracked `--evidence` file, or one git cannot answer for, is
|
|
63
86
|
// dropped: the run goes on exactly as if `--evidence` was not given, and the stderr
|
|
64
87
|
// summary carries the note `evidence-ignored`. A tracked out path, or one git cannot
|
|
@@ -68,14 +91,16 @@
|
|
|
68
91
|
//
|
|
69
92
|
// Exit codes:
|
|
70
93
|
// 0 ok
|
|
71
|
-
// 1 usage error — stdout entirely empty, usage on stderr
|
|
94
|
+
// 1 usage error — stdout entirely empty, usage on stderr (a `publish` usage error is
|
|
95
|
+
// exit 2 with the evidence unavailable line instead)
|
|
72
96
|
// 2 input unusable — a file could not be read (missing, not a regular file,
|
|
73
97
|
// oversize, not UTF-8), the evidence file or the state directory is
|
|
74
98
|
// malformed, or the PR body's test-plan block cannot serve as the plan.
|
|
75
99
|
// For `publish` and `readiness` (D-OP-SCRIPT-POINTER) it is the DEGRADED
|
|
76
100
|
// 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
|
|
101
|
+
// says so. For `claim` it is the refusal line above
|
|
102
|
+
// 3 output write failed — no stdout line is printed (a `claim` write leaves the
|
|
103
|
+
// evidence file as it was)
|
|
79
104
|
// 4 remote or internal failure before any verdict — `gh pr view` failed or
|
|
80
105
|
// answered something unusable, or an unexpected internal error
|
|
81
106
|
// 5 output gate refused — `check` found the file invalid, the EVIDENCE line
|
|
@@ -223,6 +248,12 @@ const DECISIVE_REVIEW_STATES = new Set(['APPROVED', 'CHANGES_REQUESTED', 'DISMIS
|
|
|
223
248
|
const SPLICE_LINE_RE = /^SPLICE (?<outcome>ok|resplice|conflict|malformed|oversize)$/;
|
|
224
249
|
const READBACK_LINE_RE = /^READBACK (?<outcome>ok|mismatch)$/;
|
|
225
250
|
|
|
251
|
+
/** D-CLAIM-APPEND: the stdout shapes of `claim`, and the closed set of its refusal reasons. */
|
|
252
|
+
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})$/;
|
|
253
|
+
const CLAIM_REFUSAL_RE = /^CLAIM refused \((?<reason>head not one sha|evidence file tracked|evidence file unusable|plan file unusable)\)$/;
|
|
254
|
+
/** One `claim` argument: the TP, its outcome, and an exit the script keeps only as a number from 0 to 255. */
|
|
255
|
+
const CLAIM_ARG_RE = /^TP-(?<n>[1-9][0-9]{0,2})=(?<outcome>PASS|FAIL|SKIP)(?::(?<exit>[^\s]{1,12}))?$/;
|
|
256
|
+
|
|
226
257
|
const USAGE = [
|
|
227
258
|
'Usage: node verify-evidence.cjs check tp|block|exceptions|wave <file>',
|
|
228
259
|
' node verify-evidence.cjs check wave --links-only <file>',
|
|
@@ -234,6 +265,8 @@ const USAGE = [
|
|
|
234
265
|
' node verify-evidence.cjs publish --pr <n> [--publication auto|full|off|stub] [--evidence <file>]',
|
|
235
266
|
' node verify-evidence.cjs readiness --pr <n> [--require-non-author-approval true|false]',
|
|
236
267
|
' [--evidence <file>] [--out <file>]',
|
|
268
|
+
' node verify-evidence.cjs claim --evidence <file> --head <sha> [--plan <file>]',
|
|
269
|
+
' gate | TP-<n>=<PASS|FAIL|SKIP>[:<exit>] ...',
|
|
237
270
|
].join('\n');
|
|
238
271
|
|
|
239
272
|
// ---------------------------------------------------------------------------
|
|
@@ -290,7 +323,9 @@ const USAGE = [
|
|
|
290
323
|
* | { kind: 'readback', pr: number, expect: string }
|
|
291
324
|
* | { kind: 'publish', pr: number, publication: string, evidence: string | null }
|
|
292
325
|
* | { kind: 'readiness', pr: number, requireNonAuthor: 'true' | 'false' | 'unrecognised',
|
|
293
|
-
* evidence: string | null, out: string | null }
|
|
326
|
+
* evidence: string | null, out: string | null }
|
|
327
|
+
* | { kind: 'claim', evidence: string, head: string, plan: string | null, gate: boolean,
|
|
328
|
+
* tps: readonly { id: number, outcome: 'PASS' | 'FAIL' | 'SKIP', exit: string | null }[] }} ParsedArgs
|
|
294
329
|
*/
|
|
295
330
|
|
|
296
331
|
/** @type {ParsedArgs} */
|
|
@@ -423,9 +458,45 @@ function parseArgs(argv) {
|
|
|
423
458
|
// An absent flag is the ladder's "unrecognised": it can never read READY.
|
|
424
459
|
return { kind: 'readiness', pr, requireNonAuthor: needed === null ? 'unrecognised' : needed, evidence, out };
|
|
425
460
|
}
|
|
461
|
+
if (sub === 'claim') return parseClaimArgs(tail);
|
|
426
462
|
return USAGE_ARGS;
|
|
427
463
|
}
|
|
428
464
|
|
|
465
|
+
/**
|
|
466
|
+
* D-CLAIM-APPEND: `claim` takes its flags first, then either the one word `gate` or
|
|
467
|
+
* one to CAPS.TPS `TP-<n>=<outcome>[:<exit>]` arguments, each TP at most once. A bad
|
|
468
|
+
* flag or argument is a usage error. `--head` is not checked here: a value that is not
|
|
469
|
+
* one SHA is a refusal with a line, since the caller passes what its report shows.
|
|
470
|
+
*
|
|
471
|
+
* @param {readonly string[]} tail
|
|
472
|
+
* @returns {ParsedArgs}
|
|
473
|
+
*/
|
|
474
|
+
function parseClaimArgs(tail) {
|
|
475
|
+
let flagCount = 0;
|
|
476
|
+
while (flagCount < tail.length && tail[flagCount].startsWith('--')) flagCount += 2;
|
|
477
|
+
const f = parseFlags(tail.slice(0, flagCount), { '--evidence': 'value', '--head': 'value', '--plan': 'value' });
|
|
478
|
+
const evidence = f === null ? null : flagValue(f, '--evidence');
|
|
479
|
+
const head = f === null ? null : flagValue(f, '--head');
|
|
480
|
+
const plan = f === null ? null : flagValue(f, '--plan');
|
|
481
|
+
const items = tail.slice(flagCount);
|
|
482
|
+
if (evidence === null || head === null || items.length === 0 || items.length > CAPS.TPS) return USAGE_ARGS;
|
|
483
|
+
if (items.length === 1 && items[0] === 'gate') {
|
|
484
|
+
return plan === null ? { kind: 'claim', evidence, head, plan, gate: true, tps: [] } : USAGE_ARGS;
|
|
485
|
+
}
|
|
486
|
+
const tps = [];
|
|
487
|
+
for (const item of items) {
|
|
488
|
+
const m = CLAIM_ARG_RE.exec(item);
|
|
489
|
+
if (m === null || m.groups === undefined) return USAGE_ARGS;
|
|
490
|
+
tps.push({
|
|
491
|
+
id: Number(m.groups.n),
|
|
492
|
+
outcome: /** @type {'PASS' | 'FAIL' | 'SKIP'} */ (m.groups.outcome),
|
|
493
|
+
exit: m.groups.exit === undefined ? null : m.groups.exit,
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
if (new Set(tps.map(tp => tp.id)).size !== tps.length) return USAGE_ARGS;
|
|
497
|
+
return { kind: 'claim', evidence, head, plan, gate: false, tps };
|
|
498
|
+
}
|
|
499
|
+
|
|
429
500
|
// ---------------------------------------------------------------------------
|
|
430
501
|
// Bounded file I/O
|
|
431
502
|
// ---------------------------------------------------------------------------
|
|
@@ -516,6 +587,44 @@ function writeTextFile(filePath, text) {
|
|
|
516
587
|
}
|
|
517
588
|
}
|
|
518
589
|
|
|
590
|
+
/**
|
|
591
|
+
* Append `text` to `filePath` in one write, creating the file when it is absent. The same
|
|
592
|
+
* refusals as writeTextFile apply: a path that exists and is not a regular file, a link
|
|
593
|
+
* put in its place (O_NOFOLLOW), a FIFO (O_NONBLOCK). A write that does not complete
|
|
594
|
+
* cuts the file back to its length before the append, or removes the file when this call
|
|
595
|
+
* created it, so a failed append leaves no part of `text`. Returns whether it was written.
|
|
596
|
+
*
|
|
597
|
+
* @param {string} filePath
|
|
598
|
+
* @param {string} text
|
|
599
|
+
* @returns {boolean}
|
|
600
|
+
*/
|
|
601
|
+
function appendTextFile(filePath, text) {
|
|
602
|
+
let fd;
|
|
603
|
+
let created = false;
|
|
604
|
+
try {
|
|
605
|
+
const st = fs.lstatSync(filePath, { throwIfNoEntry: false });
|
|
606
|
+
if (st !== undefined && !st.isFile()) return false;
|
|
607
|
+
created = st === undefined;
|
|
608
|
+
const c = fs.constants;
|
|
609
|
+
fd = fs.openSync(filePath, c.O_WRONLY | c.O_APPEND | c.O_CREAT | (c.O_NOFOLLOW || 0) | (c.O_NONBLOCK || 0), 0o600);
|
|
610
|
+
const before = fs.fstatSync(fd);
|
|
611
|
+
if (!before.isFile()) return false;
|
|
612
|
+
const bytes = Buffer.from(text, 'utf8');
|
|
613
|
+
try {
|
|
614
|
+
if (fs.writeSync(fd, bytes) === bytes.length) return true;
|
|
615
|
+
} catch (_) { /* cut back below */ }
|
|
616
|
+
try { fs.ftruncateSync(fd, before.size); } catch (_) { /* the write already failed */ }
|
|
617
|
+
if (created) fs.unlinkSync(filePath);
|
|
618
|
+
return false;
|
|
619
|
+
} catch (_) {
|
|
620
|
+
return false;
|
|
621
|
+
} finally {
|
|
622
|
+
if (fd !== undefined) {
|
|
623
|
+
try { fs.closeSync(fd); } catch (_) { /* the write already decided */ }
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
519
628
|
/**
|
|
520
629
|
* @param {string} dir
|
|
521
630
|
* @returns {boolean}
|
|
@@ -753,13 +862,18 @@ function trackedState(io, file) {
|
|
|
753
862
|
* the write failure (exit 3): no file is written. An `--evidence` file that is not
|
|
754
863
|
* clearly untracked is dropped, so the run goes on exactly as if `--evidence` was not
|
|
755
864
|
* given; the stderr summary then carries the closed note `evidence-ignored`. Stdout
|
|
756
|
-
* keeps its grammar.
|
|
865
|
+
* keeps its grammar. A `claim` run has its own refusal line for each of these cases
|
|
866
|
+
* (claimRefusalReason).
|
|
757
867
|
*
|
|
758
868
|
* @param {Io} io
|
|
759
869
|
* @param {ParsedArgs} args
|
|
760
870
|
* @returns {{ args: ParsedArgs } | { outcome: Outcome }}
|
|
761
871
|
*/
|
|
762
872
|
function admitPaths(io, args) {
|
|
873
|
+
if (args.kind === 'claim') {
|
|
874
|
+
const reason = claimRefusalReason(io, args);
|
|
875
|
+
return reason === null ? { args } : { outcome: claimRefusal(reason) };
|
|
876
|
+
}
|
|
763
877
|
if (args.kind !== 'verify' && args.kind !== 'publish' && args.kind !== 'readiness') return { args };
|
|
764
878
|
const outs = args.kind === 'verify' ? [args.blockOut, args.commentOut, args.staleOut]
|
|
765
879
|
: args.kind === 'readiness' ? [args.out] : [];
|
|
@@ -1424,7 +1538,16 @@ function runsAt(io, memo, sha) {
|
|
|
1424
1538
|
*/
|
|
1425
1539
|
function loadEvidence(file) {
|
|
1426
1540
|
const text = readTextFile(file);
|
|
1427
|
-
|
|
1541
|
+
return text === null ? null : parseEvidenceText(text);
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
/**
|
|
1545
|
+
* The sections of an evidence file's text, parsed (see loadEvidence).
|
|
1546
|
+
*
|
|
1547
|
+
* @param {string} text
|
|
1548
|
+
* @returns {EvidenceInput | null} null when unusable
|
|
1549
|
+
*/
|
|
1550
|
+
function parseEvidenceText(text) {
|
|
1428
1551
|
const sections = PE.evidenceSections(text);
|
|
1429
1552
|
if (!sections.ok) return null;
|
|
1430
1553
|
let plan = null;
|
|
@@ -2026,6 +2149,21 @@ const COMPLIANCE_UNREADABLE_LINE = 'TRACEABILITY: DEGRADED (compliance state unr
|
|
|
2026
2149
|
/** Every line a `publish` that did not run may print. The boundary admits each one with exit 2 only. */
|
|
2027
2150
|
const PUBLISH_REFUSAL_LINES = Object.freeze([EVIDENCE_UNAVAILABLE_LINE, COMPLIANCE_OFF_LINE, COMPLIANCE_UNREADABLE_LINE]);
|
|
2028
2151
|
|
|
2152
|
+
/** The answer of every `publish` that could not give its block: the one line, exit 2. */
|
|
2153
|
+
const PUBLISH_UNAVAILABLE = Object.freeze({ code: EXIT_CODES.INPUT_UNUSABLE, stdout: EVIDENCE_UNAVAILABLE_LINE + '\n' });
|
|
2154
|
+
|
|
2155
|
+
/**
|
|
2156
|
+
* Whether argv asks for `publish`. The boundary reads it after any failure, a usage
|
|
2157
|
+
* error and a throw included, so settleOutcome can answer a failed `publish` with
|
|
2158
|
+
* PUBLISH_UNAVAILABLE.
|
|
2159
|
+
*
|
|
2160
|
+
* @param {readonly string[]} argv process.argv
|
|
2161
|
+
* @returns {boolean}
|
|
2162
|
+
*/
|
|
2163
|
+
function isPublishRun(argv) {
|
|
2164
|
+
return Array.isArray(argv) && argv[2] === 'publish';
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2029
2167
|
/**
|
|
2030
2168
|
* @typedef {{ status: string, reason: string | null }} Part
|
|
2031
2169
|
* One half of the `publish` answer: a closed status, or `DEGRADED` with its reason.
|
|
@@ -2077,8 +2215,26 @@ function traceReasons(body, comment) {
|
|
|
2077
2215
|
}
|
|
2078
2216
|
|
|
2079
2217
|
/**
|
|
2080
|
-
* The
|
|
2081
|
-
*
|
|
2218
|
+
* The test-plan tally that both scripted blocks print: `<verified>/<total>`, the given
|
|
2219
|
+
* word, then the two counts that make up `verified`. The readiness block prints it with
|
|
2220
|
+
* no word and the publish block with ` verified`.
|
|
2221
|
+
*
|
|
2222
|
+
* @param {{ total: number, counts: Readonly<Record<string, number>> }} fields the parsed EVIDENCE line
|
|
2223
|
+
* @param {'' | ' verified'} word
|
|
2224
|
+
* @returns {string}
|
|
2225
|
+
*/
|
|
2226
|
+
function planTally(fields, word) {
|
|
2227
|
+
const verifiedCi = fields.counts['VERIFIED-CI'];
|
|
2228
|
+
const attestedLocal = fields.counts['ATTESTED-LOCAL'];
|
|
2229
|
+
return (verifiedCi + attestedLocal) + '/' + fields.total + word
|
|
2230
|
+
+ ' (VERIFIED-CI ' + verifiedCi + ', ATTESTED-LOCAL ' + attestedLocal + ')';
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
/**
|
|
2234
|
+
* The `## PR Evidence` block of the update-pr-evidence contract: the EVIDENCE line, the
|
|
2235
|
+
* `Test plan:` line read from it, the Body and Comment line, then its TRACEABILITY lines.
|
|
2236
|
+
* The caller holds the precondition that `evidenceLine` parses; a line that does not is a
|
|
2237
|
+
* broken invariant, which the catch in main turns into "evidence unavailable".
|
|
2082
2238
|
*
|
|
2083
2239
|
* @param {string} evidenceLine
|
|
2084
2240
|
* @param {Part} body
|
|
@@ -2086,9 +2242,12 @@ function traceReasons(body, comment) {
|
|
|
2086
2242
|
* @returns {string}
|
|
2087
2243
|
*/
|
|
2088
2244
|
function renderPublishBlock(evidenceLine, body, comment) {
|
|
2245
|
+
const evidence = PE.parseEvidenceLine(evidenceLine);
|
|
2246
|
+
if (!evidence.ok) throw new Error('verify-evidence: unchecked evidence line');
|
|
2089
2247
|
const lines = [
|
|
2090
2248
|
'## PR Evidence',
|
|
2091
2249
|
evidenceLine,
|
|
2250
|
+
'Test plan: ' + planTally(evidence.value, ' verified'),
|
|
2092
2251
|
'**Body**: ' + partText(body) + ' · **Comment**: ' + partText(comment),
|
|
2093
2252
|
...traceReasons(body, comment).map(r => 'TRACEABILITY: DEGRADED (' + r + ')'),
|
|
2094
2253
|
];
|
|
@@ -2110,8 +2269,8 @@ function parsePart(text, statuses) {
|
|
|
2110
2269
|
|
|
2111
2270
|
/**
|
|
2112
2271
|
* 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.
|
|
2272
|
+
* exactly what renderPublishBlock prints, so a `Test plan:` line that disagrees with the
|
|
2273
|
+
* EVIDENCE line is refused. The boundary prints a block only when this accepts it.
|
|
2115
2274
|
*
|
|
2116
2275
|
* @param {string} text the whole stdout, its final newline included
|
|
2117
2276
|
* @returns {{ evidenceLine: string, body: Part, comment: Part } | null}
|
|
@@ -2119,9 +2278,9 @@ function parsePart(text, statuses) {
|
|
|
2119
2278
|
function parsePublishBlock(text) {
|
|
2120
2279
|
if (typeof text !== 'string' || !text.endsWith('\n') || text.length > 8192) return null;
|
|
2121
2280
|
const lines = text.slice(0, -1).split('\n');
|
|
2122
|
-
if (lines.length <
|
|
2281
|
+
if (lines.length < 4 || lines.length > 6 || lines[0] !== '## PR Evidence') return null;
|
|
2123
2282
|
if (!PE.parseEvidenceLine(lines[1]).ok) return null;
|
|
2124
|
-
const m = PUBLISH_PARTS_RE.exec(lines[
|
|
2283
|
+
const m = PUBLISH_PARTS_RE.exec(lines[3]);
|
|
2125
2284
|
if (m === null || m.groups === undefined) return null;
|
|
2126
2285
|
const body = parsePart(m.groups.body, BODY_STATUSES);
|
|
2127
2286
|
const comment = parsePart(m.groups.comment, COMMENT_STATUSES);
|
|
@@ -2138,7 +2297,7 @@ function parsePublishBlock(text) {
|
|
|
2138
2297
|
*/
|
|
2139
2298
|
function evidenceUnavailable(io, reason) {
|
|
2140
2299
|
io.stderr('verify-evidence: publish: ' + reason + '\n');
|
|
2141
|
-
return
|
|
2300
|
+
return PUBLISH_UNAVAILABLE;
|
|
2142
2301
|
}
|
|
2143
2302
|
|
|
2144
2303
|
/**
|
|
@@ -2826,9 +2985,7 @@ function readinessPass(io, args, deps) {
|
|
|
2826
2985
|
: String(threads.value.unresolved),
|
|
2827
2986
|
decision: decisionText,
|
|
2828
2987
|
ci: ci.status,
|
|
2829
|
-
plan:
|
|
2830
|
-
: (evidence.verifiedCi + evidence.attestedLocal) + '/' + evidence.total
|
|
2831
|
-
+ ' (VERIFIED-CI ' + evidence.verifiedCi + ', ATTESTED-LOCAL ' + evidence.attestedLocal + ')',
|
|
2988
|
+
plan: fields === null ? 'unavailable' : planTally(fields, ''),
|
|
2832
2989
|
approval: approvalText(args.requireNonAuthor, evidence),
|
|
2833
2990
|
};
|
|
2834
2991
|
return { outcome: { code: EXIT_CODES.OK, stdout: renderReadinessBlock(block) }, head };
|
|
@@ -2922,6 +3079,159 @@ function runReadiness(io, args, deps) {
|
|
|
2922
3079
|
return args.out === null ? pass.outcome : writeReadinessOut(io, args.out, args.pr, pass.outcome, pass.head, planSource(args));
|
|
2923
3080
|
}
|
|
2924
3081
|
|
|
3082
|
+
// ---------------------------------------------------------------------------
|
|
3083
|
+
// claim — D-CLAIM-APPEND
|
|
3084
|
+
// ---------------------------------------------------------------------------
|
|
3085
|
+
//
|
|
3086
|
+
// A Test or Validate report becomes claim lines in the evidence file. The caller passes
|
|
3087
|
+
// only the facts of its report (the HEAD, the gate, one outcome per TP). This script owns
|
|
3088
|
+
// the rest: the line shapes (the claim parser of pr-evidence.cjs accepts each one before
|
|
3089
|
+
// it is written), the HEAD rule, the exit range, the `## Claims` section and the append.
|
|
3090
|
+
// A claim is an input to `verify`, never a state: only pr-evidence.cjs and `verify`
|
|
3091
|
+
// assign states.
|
|
3092
|
+
|
|
3093
|
+
/**
|
|
3094
|
+
* @param {string} reason one of the CLAIM_REFUSAL_RE reasons
|
|
3095
|
+
* @returns {Outcome}
|
|
3096
|
+
*/
|
|
3097
|
+
function claimRefusal(reason) {
|
|
3098
|
+
return { code: EXIT_CODES.INPUT_UNUSABLE, stdout: 'CLAIM refused (' + reason + ')\n' };
|
|
3099
|
+
}
|
|
3100
|
+
|
|
3101
|
+
/**
|
|
3102
|
+
* The reason a `claim` run must not start, or null. Nothing is read or written before
|
|
3103
|
+
* this: the HEAD is one 40-hex SHA, each path is a path the commands build, the evidence
|
|
3104
|
+
* file sits in a folder that exists, and git says clearly that neither file is tracked
|
|
3105
|
+
* (D-TRACKED-RESULT).
|
|
3106
|
+
*
|
|
3107
|
+
* @param {Io} io
|
|
3108
|
+
* @param {Extract<ParsedArgs, { kind: 'claim' }>} args
|
|
3109
|
+
* @returns {string | null}
|
|
3110
|
+
*/
|
|
3111
|
+
function claimRefusalReason(io, args) {
|
|
3112
|
+
if (!SHA40_RE.test(args.head)) return 'head not one sha';
|
|
3113
|
+
if (!EVIDENCE_PATH_RE.test(args.evidence) || !isRealDirectory(path.dirname(path.resolve(args.evidence)))) return 'evidence file unusable';
|
|
3114
|
+
if (args.plan !== null && !EVIDENCE_PATH_RE.test(args.plan)) return 'plan file unusable';
|
|
3115
|
+
if (trackedState(io, args.evidence) !== 'untracked') return 'evidence file tracked';
|
|
3116
|
+
if (args.plan !== null && trackedState(io, args.plan) !== 'untracked') return 'plan file unusable';
|
|
3117
|
+
return null;
|
|
3118
|
+
}
|
|
3119
|
+
|
|
3120
|
+
/**
|
|
3121
|
+
* Whether one line is exactly one claim to the claim parser of pr-evidence.cjs.
|
|
3122
|
+
*
|
|
3123
|
+
* @param {string} line
|
|
3124
|
+
* @returns {boolean}
|
|
3125
|
+
*/
|
|
3126
|
+
function claimLineOk(line) {
|
|
3127
|
+
const r = PE.parseClaims(line);
|
|
3128
|
+
if (!r.ok) return false;
|
|
3129
|
+
const gates = r.value.gates;
|
|
3130
|
+
return r.value.malformed === 0 && r.value.tp.size + (gates.validate === null ? 0 : 1) + (gates.qa === null ? 0 : 1) === 1;
|
|
3131
|
+
}
|
|
3132
|
+
|
|
3133
|
+
/**
|
|
3134
|
+
* Assert a line before it is written: one claim to the claim parser. Every caller builds
|
|
3135
|
+
* the line from parts that are already checked; this is that precondition held at the sink.
|
|
3136
|
+
*
|
|
3137
|
+
* @param {string} line
|
|
3138
|
+
* @returns {string}
|
|
3139
|
+
*/
|
|
3140
|
+
function claimLine(line) {
|
|
3141
|
+
if (!claimLineOk(line)) throw new Error('verify-evidence: unchecked claim line');
|
|
3142
|
+
return line;
|
|
3143
|
+
}
|
|
3144
|
+
|
|
3145
|
+
/**
|
|
3146
|
+
* The text to append: the lines, behind the `## Claims` heading when the file has no such
|
|
3147
|
+
* section (a blank line first, when the file holds anything). The section is the last of
|
|
3148
|
+
* the file, so the lines go at its end.
|
|
3149
|
+
*
|
|
3150
|
+
* @param {string} text the evidence file as it is, '' when it is absent
|
|
3151
|
+
* @param {boolean} hasSection
|
|
3152
|
+
* @param {readonly string[]} lines
|
|
3153
|
+
* @returns {string}
|
|
3154
|
+
*/
|
|
3155
|
+
function claimAppendText(text, hasSection, lines) {
|
|
3156
|
+
const body = lines.join('\n') + '\n';
|
|
3157
|
+
if (text === '') return PE.CLAIMS_HEADING + '\n' + body;
|
|
3158
|
+
const lead = text.endsWith('\n') ? '' : '\n';
|
|
3159
|
+
return hasSection ? lead + body : lead + '\n' + PE.CLAIMS_HEADING + '\n' + body;
|
|
3160
|
+
}
|
|
3161
|
+
|
|
3162
|
+
/**
|
|
3163
|
+
* The TP numbers the plan lists: the `## Test Plan` of the `--plan` file, else of the
|
|
3164
|
+
* evidence file. A plan with no TP line, or no plan section, lists none. Null when the
|
|
3165
|
+
* `--plan` file cannot serve as a plan.
|
|
3166
|
+
*
|
|
3167
|
+
* @param {Extract<ParsedArgs, { kind: 'claim' }>} args
|
|
3168
|
+
* @param {EvidenceInput} evidence
|
|
3169
|
+
* @returns {Set<number> | null}
|
|
3170
|
+
*/
|
|
3171
|
+
function claimPlanIds(args, evidence) {
|
|
3172
|
+
let plan = evidence.plan;
|
|
3173
|
+
if (args.plan !== null) {
|
|
3174
|
+
const text = readTextFile(args.plan);
|
|
3175
|
+
const parsed = text === null ? null : PE.parsePlan(sectionOrWhole(text, 'testPlan'));
|
|
3176
|
+
if (parsed === null || (!parsed.ok && parsed.error.code !== 'empty')) return null;
|
|
3177
|
+
plan = parsed.ok ? parsed.value : { tps: [] };
|
|
3178
|
+
}
|
|
3179
|
+
return new Set((plan === null ? [] : plan.tps).map(tp => tp.id));
|
|
3180
|
+
}
|
|
3181
|
+
|
|
3182
|
+
/**
|
|
3183
|
+
* D-CLAIM-APPEND: `claim`. The checks of claimRefusalReason have passed (admitPaths).
|
|
3184
|
+
* The evidence file is read as `verify` reads it, and its `## Claims` section, when it
|
|
3185
|
+
* has one, must be its last section. Then every line is built and checked, and one append
|
|
3186
|
+
* writes them all. A run that writes no line (every TP off the plan) changes no file.
|
|
3187
|
+
*
|
|
3188
|
+
* @param {Io} io
|
|
3189
|
+
* @param {Extract<ParsedArgs, { kind: 'claim' }>} args
|
|
3190
|
+
* @returns {Outcome}
|
|
3191
|
+
*/
|
|
3192
|
+
function runClaim(io, args) {
|
|
3193
|
+
const file = path.resolve(args.evidence);
|
|
3194
|
+
/** @type {fs.Stats | undefined} */
|
|
3195
|
+
let st;
|
|
3196
|
+
try {
|
|
3197
|
+
st = fs.lstatSync(file, { throwIfNoEntry: false });
|
|
3198
|
+
} catch (_) {
|
|
3199
|
+
return claimRefusal('evidence file unusable');
|
|
3200
|
+
}
|
|
3201
|
+
if (st !== undefined && !st.isFile()) return claimRefusal('evidence file unusable');
|
|
3202
|
+
const text = st === undefined ? '' : readTextFile(file);
|
|
3203
|
+
const evidence = text === null ? null : parseEvidenceText(text);
|
|
3204
|
+
if (text === null || evidence === null) return claimRefusal('evidence file unusable');
|
|
3205
|
+
const headings = text.split('\n').map(row => row.replace(/\r$/, '')).filter(row => row === '##' || row.startsWith('## '));
|
|
3206
|
+
const hasSection = headings.includes(PE.CLAIMS_HEADING);
|
|
3207
|
+
if (hasSection && headings[headings.length - 1] !== PE.CLAIMS_HEADING) return claimRefusal('evidence file unusable');
|
|
3208
|
+
const listed = args.gate ? new Set() : claimPlanIds(args, evidence);
|
|
3209
|
+
if (listed === null) return claimRefusal('plan file unusable');
|
|
3210
|
+
|
|
3211
|
+
/** @type {string[]} */
|
|
3212
|
+
const lines = [];
|
|
3213
|
+
const tally = { PASS: 0, FAIL: 0, SKIP: 0 };
|
|
3214
|
+
if (args.gate) {
|
|
3215
|
+
lines.push(claimLine('- gate:validate PASS sha:' + args.head + ' by:validate'));
|
|
3216
|
+
tally.PASS++;
|
|
3217
|
+
}
|
|
3218
|
+
for (const tp of args.tps) {
|
|
3219
|
+
if (!listed.has(tp.id)) continue;
|
|
3220
|
+
const bare = '- TP-' + tp.id + ' ' + tp.outcome + ' sha:' + args.head + ' by:test';
|
|
3221
|
+
const withExit = tp.exit === null ? bare : bare + ' exit:' + tp.exit;
|
|
3222
|
+
lines.push(claimLine(claimLineOk(withExit) ? withExit : bare));
|
|
3223
|
+
tally[tp.outcome]++;
|
|
3224
|
+
}
|
|
3225
|
+
if (lines.length > 0 && !appendTextFile(file, claimAppendText(text, hasSection, lines))) {
|
|
3226
|
+
return refuse(io, EXIT_CODES.WRITE_FAILED, 'an output file could not be written');
|
|
3227
|
+
}
|
|
3228
|
+
const skipped = args.gate ? 0 : args.tps.length - lines.length;
|
|
3229
|
+
return {
|
|
3230
|
+
code: EXIT_CODES.OK,
|
|
3231
|
+
stdout: 'CLAIM appended:' + lines.length + ' PASS:' + tally.PASS + ' FAIL:' + tally.FAIL + ' SKIP:' + tally.SKIP + ' skipped:' + skipped + '\n',
|
|
3232
|
+
};
|
|
3233
|
+
}
|
|
3234
|
+
|
|
2925
3235
|
// ---------------------------------------------------------------------------
|
|
2926
3236
|
// main — returns {code, stdout}; never calls process.exit
|
|
2927
3237
|
// ---------------------------------------------------------------------------
|
|
@@ -3005,6 +3315,7 @@ function main(argv, deps) {
|
|
|
3005
3315
|
else if (args.kind === 'splice') outcome = runSplice(io, args);
|
|
3006
3316
|
else if (args.kind === 'publish') outcome = runPublish(io, args, d);
|
|
3007
3317
|
else if (args.kind === 'readiness') outcome = runReadiness(io, args, d);
|
|
3318
|
+
else if (args.kind === 'claim') outcome = runClaim(io, args);
|
|
3008
3319
|
else outcome = runReadback(io, args);
|
|
3009
3320
|
}
|
|
3010
3321
|
} catch (err) {
|
|
@@ -3038,12 +3349,26 @@ function errorLabel(err) {
|
|
|
3038
3349
|
/**
|
|
3039
3350
|
* D-VERIFY-STDOUT: settle whatever main() returned into what the boundary may
|
|
3040
3351
|
* 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.
|
|
3352
|
+
* the closed shapes for that code; anything else prints nothing and exits 4. A
|
|
3353
|
+
* `publish` run is the exception: it never settles to empty stdout, so what would
|
|
3354
|
+
* print nothing prints the evidence unavailable line with exit 2.
|
|
3355
|
+
*
|
|
3356
|
+
* @param {unknown} outcome
|
|
3357
|
+
* @param {boolean} [publish] true when the run is a `publish`
|
|
3358
|
+
* @returns {Outcome}
|
|
3359
|
+
*/
|
|
3360
|
+
function settleOutcome(outcome, publish) {
|
|
3361
|
+
const settled = settleShape(outcome);
|
|
3362
|
+
return publish === true && settled.stdout === '' ? PUBLISH_UNAVAILABLE : settled;
|
|
3363
|
+
}
|
|
3364
|
+
|
|
3365
|
+
/**
|
|
3366
|
+
* The shape check of settleOutcome, for every subcommand.
|
|
3042
3367
|
*
|
|
3043
3368
|
* @param {unknown} outcome
|
|
3044
3369
|
* @returns {Outcome}
|
|
3045
3370
|
*/
|
|
3046
|
-
function
|
|
3371
|
+
function settleShape(outcome) {
|
|
3047
3372
|
const o = /** @type {any} */ (outcome);
|
|
3048
3373
|
const failed = { code: EXIT_CODES.REMOTE_FAILURE, stdout: '' };
|
|
3049
3374
|
if (o === null || typeof o !== 'object' || typeof o.stdout !== 'string') return failed;
|
|
@@ -3073,6 +3398,14 @@ function settleOutcome(outcome) {
|
|
|
3073
3398
|
const passing = readback.groups.outcome === 'ok';
|
|
3074
3399
|
return o.code === (passing ? EXIT_CODES.OK : EXIT_CODES.OUTPUT_GATE_REFUSED) ? { code: o.code, stdout: o.stdout } : failed;
|
|
3075
3400
|
}
|
|
3401
|
+
const claimRefused = CLAIM_REFUSAL_RE.exec(text);
|
|
3402
|
+
if (claimRefused !== null) return o.code === EXIT_CODES.INPUT_UNUSABLE ? pass : failed;
|
|
3403
|
+
const claimed = CLAIM_APPENDED_RE.exec(text);
|
|
3404
|
+
if (claimed !== null && claimed.groups !== undefined) {
|
|
3405
|
+
const g = claimed.groups;
|
|
3406
|
+
const sums = Number(g.n) === Number(g.pass) + Number(g.fail) + Number(g.skip);
|
|
3407
|
+
return o.code === EXIT_CODES.OK && sums && Number(g.n) <= CAPS.TPS && Number(g.skipped) <= CAPS.TPS ? pass : failed;
|
|
3408
|
+
}
|
|
3076
3409
|
if (o.code !== EXIT_CODES.OK) return failed;
|
|
3077
3410
|
if (PE.parseEvidenceLine(text).ok) return { code: o.code, stdout: o.stdout };
|
|
3078
3411
|
const block = PE.parseBlock(text);
|
|
@@ -3094,7 +3427,7 @@ if (require.main === module) {
|
|
|
3094
3427
|
process.stderr.write('verify-evidence: internal error (' + errorLabel(err) + ')\n');
|
|
3095
3428
|
outcome = { code: EXIT_CODES.REMOTE_FAILURE, stdout: '' };
|
|
3096
3429
|
}
|
|
3097
|
-
const settled = settleOutcome(outcome);
|
|
3430
|
+
const settled = settleOutcome(outcome, isPublishRun(process.argv));
|
|
3098
3431
|
if (settled.stdout !== '') process.stdout.write(settled.stdout);
|
|
3099
3432
|
process.exitCode = settled.code;
|
|
3100
3433
|
}
|