@deftai/directive-core 0.105.0 → 0.107.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/dist/authz/classify.js +591 -34
- package/dist/check/gate-lists.js +2 -0
- package/dist/check/named-cause.js +1 -1
- package/dist/design-critique/exclusive-chip.d.ts +33 -0
- package/dist/design-critique/exclusive-chip.js +68 -0
- package/dist/design-critique/parent-audit.d.ts +59 -0
- package/dist/design-critique/parent-audit.js +121 -0
- package/dist/doctor/checks.d.ts +6 -0
- package/dist/doctor/checks.js +35 -0
- package/dist/forward-coverage/diff-coverage.d.ts +39 -0
- package/dist/forward-coverage/diff-coverage.js +112 -0
- package/dist/forward-coverage/diff-lines.d.ts +19 -0
- package/dist/forward-coverage/diff-lines.js +173 -0
- package/dist/forward-coverage/evaluate.d.ts +24 -9
- package/dist/forward-coverage/evaluate.js +125 -19
- package/dist/hooks/dest-form.d.ts +46 -0
- package/dist/hooks/dest-form.js +575 -0
- package/dist/hooks/dispatcher.d.ts +17 -1
- package/dist/hooks/dispatcher.js +82 -11
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.js +1 -0
- package/dist/init-deposit/agent-hooks.d.ts +1 -1
- package/dist/init-deposit/agent-hooks.js +1 -1
- package/dist/intake/clause-derivation.d.ts +9 -0
- package/dist/intake/clause-derivation.js +30 -3
- package/dist/intake/github-auth-modes-cli.js +14 -2
- package/dist/intake/github-auth-modes.d.ts +54 -9
- package/dist/intake/github-auth-modes.js +326 -123
- package/dist/lifecycle/completed-tracked-on-delivery.d.ts +26 -0
- package/dist/lifecycle/completed-tracked-on-delivery.js +37 -14
- package/dist/lifecycle/completed-write-guard.d.ts +48 -0
- package/dist/lifecycle/completed-write-guard.js +373 -0
- package/dist/lifecycle/index.d.ts +1 -0
- package/dist/lifecycle/index.js +1 -0
- package/dist/lifecycle-visible/evaluate.d.ts +98 -0
- package/dist/lifecycle-visible/evaluate.js +710 -0
- package/dist/lifecycle-visible/index.d.ts +2 -0
- package/dist/lifecycle-visible/index.js +2 -0
- package/dist/literal-acceptance/capture.d.ts +16 -0
- package/dist/literal-acceptance/capture.js +78 -10
- package/dist/literal-acceptance/evaluate.js +25 -3
- package/dist/literal-acceptance/index.d.ts +2 -2
- package/dist/literal-acceptance/index.js +2 -2
- package/dist/literal-acceptance/run.js +9 -3
- package/dist/literal-acceptance/types.d.ts +6 -0
- package/dist/literal-acceptance/types.js +6 -0
- package/dist/orchestration/judgment-policy.d.ts +13 -0
- package/dist/orchestration/judgment-policy.js +15 -0
- package/dist/orchestration/probe-session.js +13 -8
- package/dist/orchestration/verify-judgment-gates.d.ts +4 -0
- package/dist/orchestration/verify-judgment-gates.js +43 -11
- package/dist/policy/ceremony-dial.js +7 -7
- package/dist/policy/host-hooks.d.ts +20 -0
- package/dist/policy/host-hooks.js +121 -2
- package/dist/policy/org-force-on-migration.js +2 -1
- package/dist/policy/plan-extensions.d.ts +1 -1
- package/dist/policy/plan-extensions.js +2 -0
- package/dist/policy/product-signal.js +6 -7
- package/dist/policy/require-human-merge.js +8 -6
- package/dist/policy/resolve.d.ts +7 -2
- package/dist/policy/resolve.js +25 -8
- package/dist/policy/runtime-authority.d.ts +18 -0
- package/dist/policy/runtime-authority.js +11 -0
- package/dist/policy/value-feedback.js +7 -8
- package/dist/policy/write-fence.js +3 -0
- package/dist/preflight-cache/evaluate.d.ts +12 -0
- package/dist/preflight-cache/evaluate.js +24 -4
- package/dist/preflight-cache/index.d.ts +1 -1
- package/dist/preflight-cache/index.js +1 -1
- package/dist/product-first-done-gate/evaluate.d.ts +4 -0
- package/dist/product-first-done-gate/evaluate.js +82 -10
- package/dist/release/closed-verb-gate.d.ts +12 -0
- package/dist/release/closed-verb-gate.js +51 -0
- package/dist/release/index.d.ts +1 -0
- package/dist/release/index.js +1 -0
- package/dist/release/pipeline.js +7 -0
- package/dist/release/types.d.ts +12 -0
- package/dist/render/constants.d.ts +2 -1
- package/dist/render/constants.js +2 -1
- package/dist/render/framework-commands.js +4 -0
- package/dist/render/spec-validate.js +23 -12
- package/dist/run-summary/types.d.ts +4 -0
- package/dist/scm/design-critique-chip.d.ts +37 -0
- package/dist/scm/design-critique-chip.js +171 -0
- package/dist/scm/index.d.ts +1 -0
- package/dist/scm/index.js +1 -0
- package/dist/scm/main.d.ts +3 -0
- package/dist/scm/main.js +15 -1
- package/dist/scm/readiness-cli.d.ts +2 -0
- package/dist/scm/readiness-cli.js +42 -1
- package/dist/scm/readiness.d.ts +2 -1
- package/dist/scm/readiness.js +7 -1
- package/dist/scope/decompose.js +2 -1
- package/dist/scope/delivery-evidence.d.ts +27 -1
- package/dist/scope/index.d.ts +1 -0
- package/dist/scope/index.js +1 -0
- package/dist/scope/lifecycle-write.d.ts +24 -0
- package/dist/scope/lifecycle-write.js +64 -0
- package/dist/scope/main.js +18 -6
- package/dist/scope/promote-from-issue.js +5 -5
- package/dist/scope/transition.js +90 -11
- package/dist/scope-provenance/evaluate.js +8 -4
- package/dist/session/ac-pass-banking.d.ts +5 -0
- package/dist/session/ac-pass-banking.js +3 -2
- package/dist/session/git.d.ts +18 -0
- package/dist/session/git.js +81 -1
- package/dist/session/occupancy.d.ts +2 -1
- package/dist/session/occupancy.js +11 -4
- package/dist/session/process-cost.d.ts +32 -10
- package/dist/session/process-cost.js +189 -1
- package/dist/session/product-state-hash.js +41 -24
- package/dist/session/ritual-entrypoint.js +16 -1
- package/dist/session/session-start.d.ts +18 -0
- package/dist/session/session-start.js +49 -2
- package/dist/session/verify-session-ritual.d.ts +46 -4
- package/dist/session/verify-session-ritual.js +118 -10
- package/dist/swarm/finalize-cohort-cli.d.ts +7 -2
- package/dist/swarm/finalize-cohort-cli.js +165 -35
- package/dist/swarm/finalize-cohort.d.ts +1 -0
- package/dist/swarm/finalize-cohort.js +14 -2
- package/dist/swarm/launch.d.ts +69 -0
- package/dist/swarm/launch.js +191 -1
- package/dist/swarm/routing-set-cli.js +2 -0
- package/dist/swarm/routing-verify.d.ts +1 -1
- package/dist/swarm/routing-verify.js +2 -2
- package/dist/triage/actions/index.d.ts +4 -0
- package/dist/triage/actions/index.js +6 -0
- package/dist/triage/bootstrap/index.js +6 -6
- package/dist/triage/evaluate/evaluate.d.ts +11 -0
- package/dist/triage/evaluate/evaluate.js +169 -0
- package/dist/triage/evaluate/github.d.ts +16 -0
- package/dist/triage/evaluate/github.js +153 -0
- package/dist/triage/evaluate/index.d.ts +9 -0
- package/dist/triage/evaluate/index.js +8 -0
- package/dist/triage/evaluate/paths.d.ts +7 -0
- package/dist/triage/evaluate/paths.js +25 -0
- package/dist/triage/evaluate/sink.d.ts +5 -0
- package/dist/triage/evaluate/sink.js +52 -0
- package/dist/triage/evaluate/types.d.ts +93 -0
- package/dist/triage/evaluate/types.js +13 -0
- package/dist/triage/evaluate/validity.d.ts +8 -0
- package/dist/triage/evaluate/validity.js +87 -0
- package/dist/triage/evaluate/value.d.ts +12 -0
- package/dist/triage/evaluate/value.js +33 -0
- package/dist/triage/evaluate/wip-census.d.ts +5 -0
- package/dist/triage/evaluate/wip-census.js +39 -0
- package/dist/triage/evaluate/worktrees.d.ts +8 -0
- package/dist/triage/evaluate/worktrees.js +68 -0
- package/dist/triage/evaluate/xbrief-refs.d.ts +8 -0
- package/dist/triage/evaluate/xbrief-refs.js +60 -0
- package/dist/triage/help/registry-data.d.ts +22 -8
- package/dist/triage/help/registry-data.js +54 -6
- package/dist/triage/index.d.ts +1 -0
- package/dist/triage/index.js +1 -0
- package/dist/triage/welcome/writers.d.ts +1 -1
- package/dist/triage/welcome/writers.js +10 -5
- package/dist/value/feedback-file.d.ts +17 -2
- package/dist/value/feedback-file.js +80 -6
- package/dist/value/readback.d.ts +4 -1
- package/dist/value/readback.js +51 -12
- package/dist/vbrief-reconcile/labels.d.ts +1 -0
- package/dist/vbrief-reconcile/labels.js +30 -0
- package/dist/vbrief-validate/schema.d.ts +4 -0
- package/dist/vbrief-validate/schema.js +1 -1
- package/dist/vbrief-validation/story-quality.d.ts +7 -0
- package/dist/vbrief-validation/story-quality.js +8 -1
- package/dist/verify-env/agent-hook-readiness.d.ts +2 -2
- package/dist/verify-env/agent-hook-readiness.js +12 -8
- package/dist/verify-env/agent-hooks-live-probe.d.ts +5 -1
- package/dist/verify-env/agent-hooks-live-probe.js +26 -5
- package/dist/verify-env/agent-hooks.js +3 -4
- package/dist/xbrief-migrate/agents-header.js +69 -7
- package/package.json +7 -3
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Refuse newly added completed/ blobs that bypass runTransition (#3679).
|
|
3
|
+
*
|
|
4
|
+
* Historical corpus is advisory (doctor). New work in the change set is hard
|
|
5
|
+
* (verify:completed-write-guard). Does not read completionProvenance and does
|
|
6
|
+
* not change verify:completed-tracked.
|
|
7
|
+
*
|
|
8
|
+
* Disk reads are capped at COMPLETED_WRITE_GUARD_MAX_BYTES so a huge
|
|
9
|
+
* contributor-controlled completed/ add fails through the guard instead of
|
|
10
|
+
* exhausting memory on the required gate path.
|
|
11
|
+
*/
|
|
12
|
+
export type CompletedWriteGuardCode = 0 | 1 | 2;
|
|
13
|
+
export interface CompletedWriteGuardFinding {
|
|
14
|
+
readonly relPath: string;
|
|
15
|
+
readonly detail: string;
|
|
16
|
+
}
|
|
17
|
+
export interface CompletedWriteGuardResult {
|
|
18
|
+
readonly code: CompletedWriteGuardCode;
|
|
19
|
+
readonly message: string;
|
|
20
|
+
readonly findings: readonly CompletedWriteGuardFinding[];
|
|
21
|
+
}
|
|
22
|
+
export interface CompletedWriteGuardOptions {
|
|
23
|
+
readonly baseRef?: string;
|
|
24
|
+
/** Inject added repo-relative POSIX paths (skips git). */
|
|
25
|
+
readonly addedFiles?: readonly string[];
|
|
26
|
+
/** Inject payloads: relPath -> raw JSON. */
|
|
27
|
+
readonly payloads?: ReadonlyMap<string, string>;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* In-repo completed xBRIEFs average ~5.5 KiB and peak near 44 KiB.
|
|
31
|
+
* 1 MiB is ~23× that peak and still refuses a multi-hundred-MB add
|
|
32
|
+
* before the bytes are loaded.
|
|
33
|
+
*/
|
|
34
|
+
export declare const COMPLETED_WRITE_GUARD_MAX_BYTES = 1048576;
|
|
35
|
+
/**
|
|
36
|
+
* Hard check: newly added completed/ artifacts must show runTransition evidence.
|
|
37
|
+
*/
|
|
38
|
+
export declare function evaluateCompletedWriteGuard(projectRoot: string, options?: CompletedWriteGuardOptions): CompletedWriteGuardResult;
|
|
39
|
+
export interface CompletedWriteCorpusResult {
|
|
40
|
+
readonly findings: readonly CompletedWriteGuardFinding[];
|
|
41
|
+
readonly scanned: number;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Corpus scan for doctor: artifacts without transition evidence.
|
|
45
|
+
* Caller marks these advisory so historical pre-stamp files do not red doctor.
|
|
46
|
+
*/
|
|
47
|
+
export declare function scanCompletedWriteCorpus(projectRoot: string): CompletedWriteCorpusResult;
|
|
48
|
+
//# sourceMappingURL=completed-write-guard.d.ts.map
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Refuse newly added completed/ blobs that bypass runTransition (#3679).
|
|
3
|
+
*
|
|
4
|
+
* Historical corpus is advisory (doctor). New work in the change set is hard
|
|
5
|
+
* (verify:completed-write-guard). Does not read completionProvenance and does
|
|
6
|
+
* not change verify:completed-tracked.
|
|
7
|
+
*
|
|
8
|
+
* Disk reads are capped at COMPLETED_WRITE_GUARD_MAX_BYTES so a huge
|
|
9
|
+
* contributor-controlled completed/ add fails through the guard instead of
|
|
10
|
+
* exhausting memory on the required gate path.
|
|
11
|
+
*/
|
|
12
|
+
import { spawnSync } from "node:child_process";
|
|
13
|
+
import { existsSync, lstatSync, readdirSync, readFileSync } from "node:fs";
|
|
14
|
+
import { dirname, join, resolve } from "node:path";
|
|
15
|
+
import { GitCommandError, GitNotFoundError } from "../encoding/git.js";
|
|
16
|
+
import { hasArtifactSuffix, LEGACY_ARTIFACT_DIR, MIGRATED_ARTIFACT_DIR, } from "../layout/resolve.js";
|
|
17
|
+
import { hasTransitionWrite, LEFTOVER_LAND_PR_REMEDIATION } from "../scope/lifecycle-write.js";
|
|
18
|
+
import { resolveDefaultBaseRef, unquoteGitPath } from "../scope-provenance/evaluate.js";
|
|
19
|
+
/**
|
|
20
|
+
* In-repo completed xBRIEFs average ~5.5 KiB and peak near 44 KiB.
|
|
21
|
+
* 1 MiB is ~23× that peak and still refuses a multi-hundred-MB add
|
|
22
|
+
* before the bytes are loaded.
|
|
23
|
+
*/
|
|
24
|
+
export const COMPLETED_WRITE_GUARD_MAX_BYTES = 1_048_576;
|
|
25
|
+
const COMPLETED_REL_RE = /^(?:xbrief|vbrief)\/completed\/[^/]+$/;
|
|
26
|
+
function normalizeRepoRelPath(raw) {
|
|
27
|
+
return raw.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
28
|
+
}
|
|
29
|
+
function isCompletedArtifactRel(relPath) {
|
|
30
|
+
const n = normalizeRepoRelPath(relPath);
|
|
31
|
+
if (!COMPLETED_REL_RE.test(n)) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
const base = n.split("/").pop() ?? "";
|
|
35
|
+
return hasArtifactSuffix(base);
|
|
36
|
+
}
|
|
37
|
+
function parsePlan(raw) {
|
|
38
|
+
try {
|
|
39
|
+
const data = JSON.parse(raw);
|
|
40
|
+
if (typeof data !== "object" || data === null || Array.isArray(data)) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
const plan = data.plan;
|
|
44
|
+
if (typeof plan !== "object" || plan === null || Array.isArray(plan)) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
return plan;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function gitEnv(projectRoot) {
|
|
54
|
+
const env = { ...process.env };
|
|
55
|
+
delete env.GIT_DIR;
|
|
56
|
+
delete env.GIT_WORK_TREE;
|
|
57
|
+
env.GIT_CEILING_DIRECTORIES = dirname(resolve(projectRoot));
|
|
58
|
+
return env;
|
|
59
|
+
}
|
|
60
|
+
function git(args, projectRoot) {
|
|
61
|
+
const result = spawnSync("git", args, {
|
|
62
|
+
cwd: projectRoot,
|
|
63
|
+
encoding: "utf8",
|
|
64
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
65
|
+
env: gitEnv(projectRoot),
|
|
66
|
+
});
|
|
67
|
+
if (result.error !== undefined) {
|
|
68
|
+
const e = result.error;
|
|
69
|
+
if (e.code === "ENOENT") {
|
|
70
|
+
throw new GitNotFoundError("'git' executable not found on PATH");
|
|
71
|
+
}
|
|
72
|
+
throw new GitCommandError(`git ${args.join(" ")} failed: ${String(e.message)}`);
|
|
73
|
+
}
|
|
74
|
+
if (result.signal !== null && result.signal !== undefined) {
|
|
75
|
+
throw new GitCommandError(`git ${args.join(" ")} killed by signal ${String(result.signal)}`);
|
|
76
|
+
}
|
|
77
|
+
const status = result.status ?? 1;
|
|
78
|
+
const stderr = String(result.stderr ?? "").trim();
|
|
79
|
+
if (status !== 0 && stderr.length > 0) {
|
|
80
|
+
return { status, stdout: `${result.stdout ?? ""}\n${stderr}` };
|
|
81
|
+
}
|
|
82
|
+
return { status, stdout: result.stdout ?? "" };
|
|
83
|
+
}
|
|
84
|
+
function addedPathsFromNameStatus(stdout) {
|
|
85
|
+
const out = [];
|
|
86
|
+
for (const line of stdout.split("\n")) {
|
|
87
|
+
const t = line.replace(/\r$/, "").trim();
|
|
88
|
+
if (t.length === 0) {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const parts = t.split("\t");
|
|
92
|
+
const status = parts[0] ?? "";
|
|
93
|
+
if (status.startsWith("A")) {
|
|
94
|
+
const path = parts[1];
|
|
95
|
+
if (path !== undefined) {
|
|
96
|
+
out.push(unquoteGitPath(path));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
else if (status.startsWith("R")) {
|
|
100
|
+
const dest = parts[2] ?? parts[1];
|
|
101
|
+
if (dest !== undefined) {
|
|
102
|
+
out.push(unquoteGitPath(dest));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return out;
|
|
107
|
+
}
|
|
108
|
+
function discoverAddedFiles(projectRoot, baseRef) {
|
|
109
|
+
const inside = git(["rev-parse", "--is-inside-work-tree"], projectRoot);
|
|
110
|
+
if (inside.status !== 0) {
|
|
111
|
+
throw new GitCommandError("not a git working tree");
|
|
112
|
+
}
|
|
113
|
+
let resolved = baseRef;
|
|
114
|
+
if (baseRef === "HEAD" || baseRef === "") {
|
|
115
|
+
const upgraded = resolveDefaultBaseRef(projectRoot);
|
|
116
|
+
if (upgraded === null) {
|
|
117
|
+
throw new GitCommandError("no merge-base ref (origin/master|main or DEFT_BASE_REF/GITHUB_BASE_REF)");
|
|
118
|
+
}
|
|
119
|
+
resolved = upgraded;
|
|
120
|
+
}
|
|
121
|
+
const hasBase = git(["rev-parse", "--verify", "-q", resolved], projectRoot).status === 0;
|
|
122
|
+
if (!hasBase) {
|
|
123
|
+
throw new GitCommandError(`base ref '${resolved}' not found; pass --base-ref`);
|
|
124
|
+
}
|
|
125
|
+
const out = new Set();
|
|
126
|
+
const range = resolved.includes("...") ? resolved : `${resolved}...HEAD`;
|
|
127
|
+
const committed = git(["diff", "--name-status", "--diff-filter=AR", range], projectRoot);
|
|
128
|
+
if (committed.status !== 0) {
|
|
129
|
+
const detail = committed.stdout.trim() || `git diff ${range} exited ${String(committed.status)}`;
|
|
130
|
+
throw new GitCommandError(`committed change-set unavailable for '${range}': ${detail}. ` +
|
|
131
|
+
"Pass --base-ref to a merge-base ancestor of HEAD.");
|
|
132
|
+
}
|
|
133
|
+
for (const p of addedPathsFromNameStatus(committed.stdout)) {
|
|
134
|
+
out.add(normalizeRepoRelPath(p));
|
|
135
|
+
}
|
|
136
|
+
const vsHead = git(["diff", "--name-status", "--diff-filter=AR", "HEAD"], projectRoot);
|
|
137
|
+
if (vsHead.status !== 0) {
|
|
138
|
+
const detail = vsHead.stdout.trim() || `git diff HEAD exited ${String(vsHead.status)}`;
|
|
139
|
+
throw new GitCommandError(`working-tree change-set unavailable: ${detail}`);
|
|
140
|
+
}
|
|
141
|
+
for (const p of addedPathsFromNameStatus(vsHead.stdout)) {
|
|
142
|
+
out.add(normalizeRepoRelPath(p));
|
|
143
|
+
}
|
|
144
|
+
const untracked = git(["ls-files", "--others", "--exclude-standard"], projectRoot);
|
|
145
|
+
if (untracked.status !== 0) {
|
|
146
|
+
const detail = untracked.stdout.trim() || `git ls-files exited ${String(untracked.status)}`;
|
|
147
|
+
throw new GitCommandError(`untracked change-set unavailable: ${detail}`);
|
|
148
|
+
}
|
|
149
|
+
for (const line of untracked.stdout.split("\n")) {
|
|
150
|
+
const p = normalizeRepoRelPath(unquoteGitPath(line));
|
|
151
|
+
if (p.length > 0) {
|
|
152
|
+
out.add(p);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return [...out];
|
|
156
|
+
}
|
|
157
|
+
function readPayload(projectRoot, relPath, payloads) {
|
|
158
|
+
const n = normalizeRepoRelPath(relPath);
|
|
159
|
+
const injected = payloads?.get(n);
|
|
160
|
+
if (injected !== undefined) {
|
|
161
|
+
if (injected.length > COMPLETED_WRITE_GUARD_MAX_BYTES) {
|
|
162
|
+
return {
|
|
163
|
+
kind: "unsafe",
|
|
164
|
+
detail: `${n}: completed/ artifact is ${String(injected.length)} bytes; ` +
|
|
165
|
+
`exceeds the ${String(COMPLETED_WRITE_GUARD_MAX_BYTES)}-byte read limit`,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
return { kind: "ok", raw: injected };
|
|
169
|
+
}
|
|
170
|
+
const parts = n.split("/").filter((part) => part.length > 0 && part !== ".");
|
|
171
|
+
let abs = resolve(projectRoot);
|
|
172
|
+
let st;
|
|
173
|
+
for (const part of parts) {
|
|
174
|
+
if (part === "..") {
|
|
175
|
+
return {
|
|
176
|
+
kind: "unsafe",
|
|
177
|
+
detail: `${n}: completed/ path escapes project root; refuse without following`,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
abs = join(abs, part);
|
|
181
|
+
try {
|
|
182
|
+
st = lstatSync(abs);
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
return { kind: "missing" };
|
|
186
|
+
}
|
|
187
|
+
if (st.isSymbolicLink()) {
|
|
188
|
+
return {
|
|
189
|
+
kind: "unsafe",
|
|
190
|
+
detail: `${n}: completed/ path contains a symlink; refuse without following`,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (st === undefined || !st.isFile()) {
|
|
195
|
+
return {
|
|
196
|
+
kind: "unsafe",
|
|
197
|
+
detail: `${n}: completed/ add is not a regular file; refuse without reading`,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
if (st.size > COMPLETED_WRITE_GUARD_MAX_BYTES) {
|
|
201
|
+
return {
|
|
202
|
+
kind: "unsafe",
|
|
203
|
+
detail: `${n}: completed/ artifact is ${String(st.size)} bytes; ` +
|
|
204
|
+
`exceeds the ${String(COMPLETED_WRITE_GUARD_MAX_BYTES)}-byte read limit`,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
try {
|
|
208
|
+
return { kind: "ok", raw: readFileSync(abs, "utf8") };
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
return { kind: "missing" };
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Hard check: newly added completed/ artifacts must show runTransition evidence.
|
|
216
|
+
*/
|
|
217
|
+
export function evaluateCompletedWriteGuard(projectRoot, options = {}) {
|
|
218
|
+
const root = resolve(projectRoot);
|
|
219
|
+
let added;
|
|
220
|
+
try {
|
|
221
|
+
if (options.addedFiles !== undefined) {
|
|
222
|
+
added = options.addedFiles.map(normalizeRepoRelPath);
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
const inside = git(["rev-parse", "--is-inside-work-tree"], root);
|
|
226
|
+
if (inside.status !== 0 || inside.stdout.trim() !== "true") {
|
|
227
|
+
return {
|
|
228
|
+
code: 0,
|
|
229
|
+
findings: [],
|
|
230
|
+
message: "verify_completed_write_guard: skipped -- not a git working tree.",
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
let baseRef = options.baseRef ?? "";
|
|
234
|
+
if (baseRef.length === 0 || baseRef === "HEAD") {
|
|
235
|
+
const resolved = resolveDefaultBaseRef(root);
|
|
236
|
+
if (resolved === null) {
|
|
237
|
+
return {
|
|
238
|
+
code: 2,
|
|
239
|
+
findings: [],
|
|
240
|
+
message: "verify_completed_write_guard: no merge-base ref found " +
|
|
241
|
+
"(origin/master|main, DEFT_BASE_REF, or GITHUB_BASE_REF). " +
|
|
242
|
+
"Pass --base-ref. Unguarded completed/ adds cannot be skipped.",
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
baseRef = resolved;
|
|
246
|
+
}
|
|
247
|
+
added = discoverAddedFiles(root, baseRef);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
catch (err) {
|
|
251
|
+
if (err instanceof GitNotFoundError) {
|
|
252
|
+
return {
|
|
253
|
+
code: 2,
|
|
254
|
+
findings: [],
|
|
255
|
+
message: "verify_completed_write_guard: 'git' executable not found on PATH.\n" +
|
|
256
|
+
" Recovery: install git or run inside a git working tree.",
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
if (err instanceof GitCommandError) {
|
|
260
|
+
const msg = err.message.toLowerCase();
|
|
261
|
+
if (msg.includes("not a git repository") ||
|
|
262
|
+
msg.includes("outside repository") ||
|
|
263
|
+
msg.includes("not a git working tree")) {
|
|
264
|
+
return {
|
|
265
|
+
code: 0,
|
|
266
|
+
findings: [],
|
|
267
|
+
message: `verify_completed_write_guard: skipped -- not a git working tree (${err.message}).`,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
return {
|
|
271
|
+
code: 2,
|
|
272
|
+
findings: [],
|
|
273
|
+
message: `verify_completed_write_guard: git failed -- ${err.message}`,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
throw err;
|
|
277
|
+
}
|
|
278
|
+
const findings = [];
|
|
279
|
+
for (const rel of added) {
|
|
280
|
+
if (!isCompletedArtifactRel(rel)) {
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
const payload = readPayload(root, rel, options.payloads);
|
|
284
|
+
if (payload.kind === "missing") {
|
|
285
|
+
findings.push({
|
|
286
|
+
relPath: rel,
|
|
287
|
+
detail: `${rel}: added under completed/ but unreadable`,
|
|
288
|
+
});
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
if (payload.kind === "unsafe") {
|
|
292
|
+
findings.push({
|
|
293
|
+
relPath: rel,
|
|
294
|
+
detail: payload.detail,
|
|
295
|
+
});
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
const plan = parsePlan(payload.raw);
|
|
299
|
+
if (plan === null) {
|
|
300
|
+
findings.push({
|
|
301
|
+
relPath: rel,
|
|
302
|
+
detail: `${rel}: added under completed/ with unreadable plan`,
|
|
303
|
+
});
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
if (!hasTransitionWrite(plan)) {
|
|
307
|
+
findings.push({
|
|
308
|
+
relPath: rel,
|
|
309
|
+
detail: `${rel}: added under completed/ without a runTransition write`,
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
if (findings.length === 0) {
|
|
314
|
+
return {
|
|
315
|
+
code: 0,
|
|
316
|
+
findings: [],
|
|
317
|
+
message: "verify_completed_write_guard: clean -- no unguarded completed/ adds",
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
return {
|
|
321
|
+
code: 1,
|
|
322
|
+
findings,
|
|
323
|
+
message: `verify_completed_write_guard: ${findings.length} unguarded completed/ add(s) (#3679).\n` +
|
|
324
|
+
findings.map((f) => ` - ${f.detail}`).join("\n") +
|
|
325
|
+
`\n${LEFTOVER_LAND_PR_REMEDIATION}`,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
function listCompletedArtifactRels(projectRoot) {
|
|
329
|
+
const out = [];
|
|
330
|
+
for (const dirName of [MIGRATED_ARTIFACT_DIR, LEGACY_ARTIFACT_DIR]) {
|
|
331
|
+
const completedDir = join(projectRoot, dirName, "completed");
|
|
332
|
+
if (!existsSync(completedDir)) {
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
let names = [];
|
|
336
|
+
try {
|
|
337
|
+
names = readdirSync(completedDir).filter((n) => hasArtifactSuffix(n));
|
|
338
|
+
}
|
|
339
|
+
catch {
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
for (const name of names) {
|
|
343
|
+
out.push(`${dirName}/completed/${name}`);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return out.sort();
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Corpus scan for doctor: artifacts without transition evidence.
|
|
350
|
+
* Caller marks these advisory so historical pre-stamp files do not red doctor.
|
|
351
|
+
*/
|
|
352
|
+
export function scanCompletedWriteCorpus(projectRoot) {
|
|
353
|
+
const findings = [];
|
|
354
|
+
const rels = listCompletedArtifactRels(projectRoot);
|
|
355
|
+
for (const rel of rels) {
|
|
356
|
+
const payload = readPayload(resolve(projectRoot), rel, undefined);
|
|
357
|
+
if (payload.kind !== "ok") {
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
const plan = parsePlan(payload.raw);
|
|
361
|
+
if (plan === null) {
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
if (!hasTransitionWrite(plan)) {
|
|
365
|
+
findings.push({
|
|
366
|
+
relPath: rel,
|
|
367
|
+
detail: `${rel}: completed/ artifact has no runTransition write (historical advisory)`,
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return { findings, scanned: rels.length };
|
|
372
|
+
}
|
|
373
|
+
//# sourceMappingURL=completed-write-guard.js.map
|
package/dist/lifecycle/index.js
CHANGED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* verify:lifecycle-visible — detect ignore/index flags that hide lifecycle roots (#3505).
|
|
3
|
+
*
|
|
4
|
+
* Warn-only by default (exit 0). Pass --enforce to fail closed.
|
|
5
|
+
* Not a task-check gate: this is a per-clone environment property.
|
|
6
|
+
*/
|
|
7
|
+
import { type GitRunner } from "../session/git.js";
|
|
8
|
+
export declare const LIFECYCLE_ROOT_PREFIXES: readonly ["xbrief", "vbrief"];
|
|
9
|
+
export declare const LIFECYCLE_STAGE_DIRS: readonly ["proposed", "pending", "active", "completed", "cancelled"];
|
|
10
|
+
export type LifecycleHideKind = "ignored" | "skip-worktree" | "assume-unchanged";
|
|
11
|
+
export interface LifecycleHideFinding {
|
|
12
|
+
readonly path: string;
|
|
13
|
+
readonly kind: LifecycleHideKind;
|
|
14
|
+
readonly source: string;
|
|
15
|
+
readonly line: number | null;
|
|
16
|
+
readonly rule: string;
|
|
17
|
+
readonly raw: string;
|
|
18
|
+
}
|
|
19
|
+
export interface LifecycleVisibleOptions {
|
|
20
|
+
readonly projectRoot: string;
|
|
21
|
+
readonly enforce?: boolean;
|
|
22
|
+
readonly runGit?: GitRunner;
|
|
23
|
+
}
|
|
24
|
+
export interface LifecycleVisibleResult {
|
|
25
|
+
readonly code: 0 | 1 | 2;
|
|
26
|
+
readonly message: string;
|
|
27
|
+
readonly stream: "stdout" | "stderr";
|
|
28
|
+
readonly findings: readonly LifecycleHideFinding[];
|
|
29
|
+
readonly enforce: boolean;
|
|
30
|
+
readonly failOpen: boolean;
|
|
31
|
+
}
|
|
32
|
+
export interface CheckIgnoreMatch {
|
|
33
|
+
readonly source: string;
|
|
34
|
+
readonly line: number;
|
|
35
|
+
readonly pattern: string;
|
|
36
|
+
readonly path: string;
|
|
37
|
+
}
|
|
38
|
+
export interface LsFilesVerboseRecord {
|
|
39
|
+
readonly tag: string;
|
|
40
|
+
readonly path: string;
|
|
41
|
+
}
|
|
42
|
+
/** Ignore globs from one file, with the directory Git applies them against. */
|
|
43
|
+
export interface IgnorePatternSource {
|
|
44
|
+
readonly baseDir: string;
|
|
45
|
+
readonly patterns: readonly string[];
|
|
46
|
+
}
|
|
47
|
+
/** Canonical lifecycle root pathspecs (`xbrief/active/`, `vbrief/pending/`, …). */
|
|
48
|
+
export declare function lifecycleRootRelPaths(): string[];
|
|
49
|
+
/** Fills `*` in ignore globs so a derived probe still matches the rule. */
|
|
50
|
+
export declare const LIFECYCLE_PROBE_STEM = "lifecycle-visible";
|
|
51
|
+
/**
|
|
52
|
+
* Date-prefixed brief-shaped fallback when no ignore-rule date glob exists.
|
|
53
|
+
* Date globs emit their own matching names (`2026-07-*` → a July file, `2025-*` → 2025).
|
|
54
|
+
*/
|
|
55
|
+
export declare const LIFECYCLE_PROBE_SENTINEL = "2026-01-01-lifecycle-visible.xbrief.json";
|
|
56
|
+
/** Legacy vbrief filename so `*.vbrief.json` / `2026-*.vbrief.json` cannot report clean. */
|
|
57
|
+
export declare const LIFECYCLE_PROBE_SENTINEL_VBRIEF = "2026-01-01-lifecycle-visible.vbrief.json";
|
|
58
|
+
/** Stage dirs plus one matching-extension sentinel under each. */
|
|
59
|
+
export declare function lifecycleIgnoreProbeRelPaths(): string[];
|
|
60
|
+
/** True when an ignore glob could hide a convention-valid brief. */
|
|
61
|
+
export declare function ignorePatternLooksLifecycleRelevant(pattern: string): boolean;
|
|
62
|
+
/** Linear `[abc]` expansion — avoid nested regex quantifiers (CodeQL js/polynomial-redos). */
|
|
63
|
+
export declare function expandGitignoreCharClasses(body: string): string;
|
|
64
|
+
/**
|
|
65
|
+
* Expand one gitignore glob into a single concrete pathname that still matches it.
|
|
66
|
+
* `*` becomes {@link LIFECYCLE_PROBE_STEM}; `**` collapses; `?` becomes `0`.
|
|
67
|
+
*/
|
|
68
|
+
export declare function expandGitignoreGlobToConcrete(pattern: string): string | null;
|
|
69
|
+
/** Join a glob-expanded probe to the ignore file's directory (Git's match root). */
|
|
70
|
+
export declare function joinIgnoreFileBase(baseDir: string, concrete: string): string;
|
|
71
|
+
/**
|
|
72
|
+
* One derived probe per relevant ignore-rule line (bounded by ignore-file size, not a numeric cap).
|
|
73
|
+
* Date globs keep their year/month so `2026-07-*` probes a July name, not the January fallback.
|
|
74
|
+
* Slash patterns keep the ignore-file directory; stage wildcards expand onto real stages.
|
|
75
|
+
*/
|
|
76
|
+
export declare function derivedLifecycleIgnoreProbesFromSources(sources: readonly IgnorePatternSource[]): string[];
|
|
77
|
+
/** Ignore-file globs → check-ignore pathspecs (one per relevant rule line). */
|
|
78
|
+
export declare function derivedLifecycleIgnoreProbes(projectRoot: string, runGit: GitRunner): string[];
|
|
79
|
+
/** Map a check-ignore pathname back to its canonical stage root, or null if outside. */
|
|
80
|
+
export declare function lifecycleRootForRelPath(relPosix: string): string | null;
|
|
81
|
+
/** Deliberate hybrid ignores under a visible root must not trip the check (#1144). */
|
|
82
|
+
export declare function isSelectiveLifecyclePath(relPosix: string): boolean;
|
|
83
|
+
/** Map git check-ignore source paths to the actionable display form. */
|
|
84
|
+
export declare function displayIgnoreSource(source: string, projectRoot: string): string;
|
|
85
|
+
/**
|
|
86
|
+
* Parse one `git check-ignore -v` line: `<source>:<linenum>:<pattern>\\t<pathname>`.
|
|
87
|
+
* Source may be a Windows absolute path with a drive colon.
|
|
88
|
+
*/
|
|
89
|
+
export declare function parseCheckIgnoreVerboseLine(line: string): CheckIgnoreMatch | null;
|
|
90
|
+
/** Parse one `git ls-files -v` record (`S path` / `h path`). */
|
|
91
|
+
export declare function parseLsFilesVerboseRecord(record: string): LsFilesVerboseRecord | null;
|
|
92
|
+
/** Index flag encoded by the `ls-files -v` status tag. */
|
|
93
|
+
export declare function indexFlagKind(tag: string): "skip-worktree" | "assume-unchanged" | null;
|
|
94
|
+
/** Session:start advisory lines — silent when clean. */
|
|
95
|
+
export declare function formatLifecycleVisibleSessionLines(result: LifecycleVisibleResult): string[];
|
|
96
|
+
/** Evaluate whether lifecycle roots are hidden by ignore rules or index flags (#3505). */
|
|
97
|
+
export declare function evaluateLifecycleVisible(options: LifecycleVisibleOptions): LifecycleVisibleResult;
|
|
98
|
+
//# sourceMappingURL=evaluate.d.ts.map
|