@webpresso/agent-kit 3.3.3 → 3.3.4
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/THIRD-PARTY-NOTICES.md +12 -10
- package/catalog/agent/rules/changeset-release.md +15 -4
- package/catalog/agent/rules/managed-tool-binaries.md +67 -0
- package/catalog/agent/rules/parallel-orchestration-quota.md +99 -1
- package/catalog/agent/rules/pre-implementation.md +36 -20
- package/catalog/agent/rules/rtk-routing.md +22 -16
- package/catalog/agent/skills/autopilot/SKILL.md +4 -3
- package/catalog/agent/skills/ultragoal/SKILL.md +11 -9
- package/dist/esm/audit/hook-surface.js +5 -1
- package/dist/esm/audit/repo-guardrails.d.ts +8 -0
- package/dist/esm/audit/repo-guardrails.js +71 -1
- package/dist/esm/blueprint/core/parser.js +38 -12
- package/dist/esm/blueprint/execution/artifacts.js +8 -4
- package/dist/esm/blueprint/execution/frontmatter-keys.d.ts +17 -0
- package/dist/esm/blueprint/execution/frontmatter-keys.js +24 -0
- package/dist/esm/blueprint/execution/metadata.js +8 -4
- package/dist/esm/blueprint/lifecycle/audit.d.ts +21 -0
- package/dist/esm/blueprint/lifecycle/audit.js +89 -3
- package/dist/esm/blueprint/lifecycle/review-provenance.js +63 -10
- package/dist/esm/blueprint/lifecycle/rollout-anchor.d.ts +22 -0
- package/dist/esm/blueprint/lifecycle/rollout-anchor.js +37 -1
- package/dist/esm/blueprint/markdown/blocked-line.d.ts +49 -0
- package/dist/esm/blueprint/markdown/blocked-line.js +63 -0
- package/dist/esm/blueprint/markdown/helpers.js +10 -4
- package/dist/esm/blueprint/tracked-document/parser.js +3 -3
- package/dist/esm/blueprint/trust/gate-row.d.ts +55 -0
- package/dist/esm/blueprint/trust/gate-row.js +113 -0
- package/dist/esm/blueprint/trust/promotion.d.ts +17 -0
- package/dist/esm/blueprint/trust/promotion.js +19 -19
- package/dist/esm/build/atomic-file-copy.d.ts +32 -0
- package/dist/esm/build/atomic-file-copy.js +44 -0
- package/dist/esm/build/vendor-oxlint-plugins.d.ts +8 -0
- package/dist/esm/build/vendor-oxlint-plugins.js +53 -0
- package/dist/esm/cli/commands/blueprint/mutations.d.ts +15 -0
- package/dist/esm/cli/commands/blueprint/mutations.js +8 -2
- package/dist/esm/cli/commands/compile.d.ts +13 -0
- package/dist/esm/cli/commands/compile.js +182 -62
- package/dist/esm/cli/commands/format.js +17 -1
- package/dist/esm/cli/commands/gain/index.js +10 -3
- package/dist/esm/cli/commands/init/gitignore-patcher.js +6 -0
- package/dist/esm/cli/commands/init/index.js +29 -48
- package/dist/esm/cli/commands/init/mcp-spec.d.ts +48 -13
- package/dist/esm/cli/commands/init/mcp-spec.js +86 -30
- package/dist/esm/cli/commands/init/merge.js +30 -10
- package/dist/esm/cli/commands/init/scaffolders/codex-mcp/index.d.ts +56 -31
- package/dist/esm/cli/commands/init/scaffolders/codex-mcp/index.js +180 -43
- package/dist/esm/cli/commands/init/scaffolders/rtk/index.d.ts +17 -5
- package/dist/esm/cli/commands/init/scaffolders/rtk/index.js +56 -75
- package/dist/esm/cli/commands/init/scaffolders/runtime-check/index.js +7 -6
- package/dist/esm/cli/commands/init/untracked-collision.d.ts +12 -1
- package/dist/esm/cli/commands/init/untracked-collision.js +23 -1
- package/dist/esm/cli/commands/pr-help.js +1 -1
- package/dist/esm/cli/commands/pr.js +18 -4
- package/dist/esm/cli/commands/review.js +60 -6
- package/dist/esm/cli/commands/worktree/router-dispatch.d.ts +24 -2
- package/dist/esm/cli/commands/worktree/router-dispatch.js +80 -48
- package/dist/esm/cli/commands/worktree/router.js +5 -2
- package/dist/esm/cli/direct-provider-launch.js +12 -4
- package/dist/esm/compiler/flatten.d.ts +10 -1
- package/dist/esm/compiler/flatten.js +14 -4
- package/dist/esm/compose/admin-blocks-registry-integrity.d.ts +22 -0
- package/dist/esm/compose/admin-blocks-registry-integrity.js +90 -0
- package/dist/esm/compose/compose.d.ts +8 -0
- package/dist/esm/compose/compose.js +115 -8
- package/dist/esm/compose/ops-report-schema.js +17 -3
- package/dist/esm/compose/registry-resolve.d.ts +17 -0
- package/dist/esm/compose/registry-resolve.js +78 -11
- package/dist/esm/config/oxlint/graphql-conventions.js +2 -5
- package/dist/esm/config/oxlint/import-hygiene.js +21 -8
- package/dist/esm/config/oxlint/oxlint-config.fixture.d.ts +1 -1
- package/dist/esm/config/oxlint/oxlint-config.fixture.js +2 -3
- package/dist/esm/config/oxlint/oxlintrc.d.ts +5 -7
- package/dist/esm/config/oxlint/oxlintrc.js +18 -21
- package/dist/esm/config/oxlint/path-roles.js +53 -0
- package/dist/esm/config/oxlint/query-patterns.js +2 -5
- package/dist/esm/daemon/domains/blueprint-exec-ledger.d.ts +86 -0
- package/dist/esm/daemon/domains/blueprint-exec-ledger.js +134 -0
- package/dist/esm/daemon/domains/blueprint-exec.d.ts +106 -0
- package/dist/esm/daemon/domains/blueprint-exec.js +161 -0
- package/dist/esm/daemon/domains/launch.d.ts +25 -0
- package/dist/esm/daemon/domains/launch.js +30 -1
- package/dist/esm/daemon/handlers/blueprints-handlers.d.ts +12 -1
- package/dist/esm/daemon/handlers/blueprints-handlers.js +148 -6
- package/dist/esm/daemon/handlers/product.d.ts +10 -0
- package/dist/esm/daemon/handlers/product.js +10 -1
- package/dist/esm/daemon/methods.d.ts +21 -1
- package/dist/esm/daemon/methods.js +20 -0
- package/dist/esm/daemon/protocol/contract.d.ts +58 -0
- package/dist/esm/daemon/protocol/contract.js +38 -0
- package/dist/esm/daemon/server.js +7 -0
- package/dist/esm/docs-linter/blueprint-plan.js +2 -2
- package/dist/esm/errors/wp-error.js +21 -2
- package/dist/esm/hooks/doctor.d.ts +11 -0
- package/dist/esm/hooks/doctor.js +114 -45
- package/dist/esm/hooks/pretool-guard/dev-routing.js +1 -1
- package/dist/esm/hooks/pretool-guard/rtk-rewrite.d.ts +24 -0
- package/dist/esm/hooks/pretool-guard/rtk-rewrite.js +50 -0
- package/dist/esm/hooks/pretool-guard/runner.js +6 -0
- package/dist/esm/hooks/status/index.js +1 -1
- package/dist/esm/mcp/blueprint/_shared/lifecycle.js +39 -11
- package/dist/esm/mcp/blueprint/handlers/review-log.d.ts +14 -7
- package/dist/esm/mcp/blueprint/handlers/review-log.js +46 -22
- package/dist/esm/mcp/blueprint/registration.js +1 -1
- package/dist/esm/mcp/tools/_shared/redact.d.ts +22 -0
- package/dist/esm/mcp/tools/_shared/redact.js +114 -8
- package/dist/esm/mcp/tools/gain.js +28 -2
- package/dist/esm/mcp/tools/worktree.js +65 -3
- package/dist/esm/mcp/tools/wp-ui-compose.js +38 -4
- package/dist/esm/mcp/tools/wp-ui-preview.js +14 -7
- package/dist/esm/review/lifecycle-writers.d.ts +76 -0
- package/dist/esm/review/lifecycle-writers.js +330 -0
- package/dist/esm/review/subject.js +66 -32
- package/dist/esm/status/snapshot.d.ts +5 -0
- package/dist/esm/status/snapshot.js +27 -2
- package/dist/esm/test/shard-durations.json +0 -1
- package/dist/esm/test-helpers/global-setup.js +38 -3
- package/dist/esm/tool-runtime/managed-rtk.d.ts +44 -0
- package/dist/esm/tool-runtime/managed-rtk.js +275 -0
- package/dist/esm/tool-runtime/resolve-runner.js +18 -194
- package/dist/esm/utils/package-root.d.ts +32 -0
- package/dist/esm/utils/package-root.js +82 -0
- package/dist/esm/worktrees/git-metadata.d.ts +41 -0
- package/dist/esm/worktrees/git-metadata.js +48 -0
- package/package.json +16 -21
- package/dist/esm/config/oxlint/code-safety.d.ts +0 -18
- package/dist/esm/config/oxlint/foundation-purity.d.ts +0 -21
- package/dist/esm/config/oxlint/graphql-conventions.d.ts +0 -23
- package/dist/esm/config/oxlint/import-hygiene.d.ts +0 -34
- package/dist/esm/config/oxlint/index.d.ts +0 -22
- package/dist/esm/config/oxlint/index.js +0 -28
- package/dist/esm/config/oxlint/monorepo-paths.d.ts +0 -22
- package/dist/esm/config/oxlint/query-patterns.d.ts +0 -24
- package/dist/esm/config/oxlint/testing-quality.d.ts +0 -35
- package/dist/esm/config/oxlint/tier-boundaries.d.ts +0 -35
- package/dist/esm/output-transforms/rulesync.d.ts +0 -2
- package/dist/esm/output-transforms/rulesync.js +0 -79
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import matter from "gray-matter";
|
|
8
8
|
import { planFrontmatterSchema, taskStatusSchema } from "#core/schema";
|
|
9
|
+
import { extractBlockedReason } from "#markdown/blocked-line";
|
|
9
10
|
import { buildTaskHeadingRegex, isTaskHeaderLine } from "#markdown/task-heading";
|
|
10
11
|
function parseWithSchema(markdown, schema) {
|
|
11
12
|
const { data: rawData, content } = matter(markdown);
|
|
@@ -87,7 +88,13 @@ const SERIALIZE_OWNED_FRONTMATTER_KEYS = new Set([
|
|
|
87
88
|
"progress",
|
|
88
89
|
"completed_at",
|
|
89
90
|
]);
|
|
90
|
-
|
|
91
|
+
// `\r?$` is load-bearing on a CRLF checkout: `blueprint.raw` is stored verbatim
|
|
92
|
+
// and split on "\n", so every original line still carries a trailing `\r`. `.`
|
|
93
|
+
// does not match a line terminator, so without `\r?` the anchor cannot match,
|
|
94
|
+
// NO originals are collected, preservation bails, and matter.stringify's
|
|
95
|
+
// re-quoted scalars survive — silently invalidating every recorded approval on
|
|
96
|
+
// that checkout. `FRONTMATTER_MULTILINE_KEY` below already carries `\r?$`.
|
|
97
|
+
const FRONTMATTER_SCALAR_LINE = /^([A-Za-z0-9_]+):[ \t]+\S.*\r?$/;
|
|
91
98
|
function quoteGeneratedProgressScalar(serialized, progress) {
|
|
92
99
|
if (!progress)
|
|
93
100
|
return serialized;
|
|
@@ -108,7 +115,10 @@ function preserveOriginalFrontmatterScalars(serialized, originalRaw) {
|
|
|
108
115
|
for (const line of originalFrontmatter.split("\n")) {
|
|
109
116
|
const match = FRONTMATTER_SCALAR_LINE.exec(line);
|
|
110
117
|
if (match && !SERIALIZE_OWNED_FRONTMATTER_KEYS.has(match[1])) {
|
|
111
|
-
|
|
118
|
+
// Store without the source's line ending; the substitution below re-applies
|
|
119
|
+
// whichever ending the SERIALIZED document uses, so a CRLF original is
|
|
120
|
+
// never spliced into an LF document (or vice versa) as a mixed ending.
|
|
121
|
+
originalScalars.set(match[1], line.replace(/\r$/, ""));
|
|
112
122
|
}
|
|
113
123
|
}
|
|
114
124
|
if (originalScalars.size === 0)
|
|
@@ -116,13 +126,30 @@ function preserveOriginalFrontmatterScalars(serialized, originalRaw) {
|
|
|
116
126
|
const block = /^(---\r?\n)([\s\S]*?)(\r?\n---(?:\r?\n|$))/.exec(serialized);
|
|
117
127
|
if (!block)
|
|
118
128
|
return serialized;
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
129
|
+
const lines = block[2].split("\n");
|
|
130
|
+
const rebuiltLines = [];
|
|
131
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
132
|
+
const line = lines[index];
|
|
122
133
|
const match = FRONTMATTER_SCALAR_LINE.exec(line);
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
134
|
+
const originalScalar = match && originalScalars.get(match[1]);
|
|
135
|
+
if (!originalScalar) {
|
|
136
|
+
rebuiltLines.push(line);
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
rebuiltLines.push(line.endsWith("\r") ? `${originalScalar}\r` : originalScalar);
|
|
140
|
+
// A freshly re-dumped value for this key may not fit on one line: long
|
|
141
|
+
// plain/quoted scalars get YAML-folded by js-yaml into indented
|
|
142
|
+
// continuation lines directly beneath the `key:` line (no `key:` prefix
|
|
143
|
+
// of their own). We are replacing the whole value wholesale with the
|
|
144
|
+
// original single-line scalar, so those continuation lines must be
|
|
145
|
+
// dropped too — otherwise they survive as an orphaned mapping fragment
|
|
146
|
+
// right after an already-complete (and possibly already-quote-closed)
|
|
147
|
+
// scalar, which is invalid YAML.
|
|
148
|
+
while (index + 1 < lines.length && /^[ \t]/.test(lines[index + 1])) {
|
|
149
|
+
index += 1;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const rebuilt = rebuiltLines.join("\n");
|
|
126
153
|
return `${serialized.slice(0, block.index)}${block[1]}${rebuilt}${block[3]}${serialized.slice(block.index + block[0].length)}`;
|
|
127
154
|
}
|
|
128
155
|
// Same residual as scalars, for multi-line blocks (YAML arrays/maps under a
|
|
@@ -328,11 +355,10 @@ function extractDepends(section) {
|
|
|
328
355
|
return ids.filter((id) => id !== "");
|
|
329
356
|
}
|
|
330
357
|
function extractBlocked(section) {
|
|
331
|
-
const
|
|
332
|
-
if (
|
|
358
|
+
const blockedText = extractBlockedReason(section);
|
|
359
|
+
if (blockedText === undefined)
|
|
333
360
|
return undefined;
|
|
334
|
-
|
|
335
|
-
if (blockedText === "" || blockedText.toLowerCase() === "none") {
|
|
361
|
+
if (blockedText.toLowerCase() === "none") {
|
|
336
362
|
return undefined;
|
|
337
363
|
}
|
|
338
364
|
return blockedText;
|
|
@@ -29,16 +29,20 @@ export function readBlueprintExecutionArtifacts(markdown) {
|
|
|
29
29
|
};
|
|
30
30
|
}
|
|
31
31
|
export function writeBlueprintExecutionArtifacts(markdown, artifacts) {
|
|
32
|
-
|
|
32
|
+
// Typed against the shared key union so a key can never be written here
|
|
33
|
+
// without also entering the plan-subject normalizer's strip set.
|
|
34
|
+
const fields = {
|
|
33
35
|
execution_artifacts: artifacts.artifacts,
|
|
34
36
|
execution_log_path: artifacts.logPath,
|
|
35
37
|
execution_verifications: artifacts.verifications,
|
|
36
|
-
}
|
|
38
|
+
};
|
|
39
|
+
return setBlueprintFrontmatterFields(markdown, fields);
|
|
37
40
|
}
|
|
38
41
|
export function clearBlueprintExecutionArtifacts(markdown) {
|
|
39
|
-
|
|
42
|
+
const fields = {
|
|
40
43
|
execution_artifacts: undefined,
|
|
41
44
|
execution_log_path: undefined,
|
|
42
45
|
execution_verifications: undefined,
|
|
43
|
-
}
|
|
46
|
+
};
|
|
47
|
+
return setBlueprintFrontmatterFields(markdown, fields);
|
|
44
48
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single owner of the `execution_*` blueprint frontmatter key names.
|
|
3
|
+
*
|
|
4
|
+
* The execution backend writes these keys into `_overview.md`
|
|
5
|
+
* (`./metadata.js`, `./artifacts.js`), and the plan-subject normalizer
|
|
6
|
+
* (`#review/subject.js`) strips them so starting or advancing an execution does
|
|
7
|
+
* not dismiss a recorded plan approval. Both sides import this list, so a key
|
|
8
|
+
* added to one side without the other is a typecheck error rather than a silent
|
|
9
|
+
* digest leak.
|
|
10
|
+
*
|
|
11
|
+
* Deliberately dependency-free so the normalizer can import it without pulling
|
|
12
|
+
* in the lifecycle engine.
|
|
13
|
+
*/
|
|
14
|
+
export declare const EXECUTION_METADATA_FRONTMATTER_KEYS: readonly ["execution_backend", "execution_id", "execution_status", "execution_updated_at"];
|
|
15
|
+
export type ExecutionMetadataFrontmatterKey = (typeof EXECUTION_METADATA_FRONTMATTER_KEYS)[number];
|
|
16
|
+
export declare const EXECUTION_ARTIFACTS_FRONTMATTER_KEYS: readonly ["execution_artifacts", "execution_log_path", "execution_verifications"];
|
|
17
|
+
export type ExecutionArtifactsFrontmatterKey = (typeof EXECUTION_ARTIFACTS_FRONTMATTER_KEYS)[number];
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single owner of the `execution_*` blueprint frontmatter key names.
|
|
3
|
+
*
|
|
4
|
+
* The execution backend writes these keys into `_overview.md`
|
|
5
|
+
* (`./metadata.js`, `./artifacts.js`), and the plan-subject normalizer
|
|
6
|
+
* (`#review/subject.js`) strips them so starting or advancing an execution does
|
|
7
|
+
* not dismiss a recorded plan approval. Both sides import this list, so a key
|
|
8
|
+
* added to one side without the other is a typecheck error rather than a silent
|
|
9
|
+
* digest leak.
|
|
10
|
+
*
|
|
11
|
+
* Deliberately dependency-free so the normalizer can import it without pulling
|
|
12
|
+
* in the lifecycle engine.
|
|
13
|
+
*/
|
|
14
|
+
export const EXECUTION_METADATA_FRONTMATTER_KEYS = [
|
|
15
|
+
"execution_backend",
|
|
16
|
+
"execution_id",
|
|
17
|
+
"execution_status",
|
|
18
|
+
"execution_updated_at",
|
|
19
|
+
];
|
|
20
|
+
export const EXECUTION_ARTIFACTS_FRONTMATTER_KEYS = [
|
|
21
|
+
"execution_artifacts",
|
|
22
|
+
"execution_log_path",
|
|
23
|
+
"execution_verifications",
|
|
24
|
+
];
|
|
@@ -28,18 +28,22 @@ export function readBlueprintExecutionMetadata(markdown) {
|
|
|
28
28
|
};
|
|
29
29
|
}
|
|
30
30
|
export function writeBlueprintExecutionMetadata(markdown, metadata) {
|
|
31
|
-
|
|
31
|
+
// Typed against the shared key union so a key can never be written here
|
|
32
|
+
// without also entering the plan-subject normalizer's strip set.
|
|
33
|
+
const fields = {
|
|
32
34
|
execution_backend: metadata.backend,
|
|
33
35
|
execution_id: metadata.executionId,
|
|
34
36
|
execution_status: metadata.status,
|
|
35
37
|
execution_updated_at: metadata.updatedAt,
|
|
36
|
-
}
|
|
38
|
+
};
|
|
39
|
+
return setBlueprintFrontmatterFields(markdown, fields);
|
|
37
40
|
}
|
|
38
41
|
export function clearBlueprintExecutionMetadata(markdown) {
|
|
39
|
-
|
|
42
|
+
const fields = {
|
|
40
43
|
execution_backend: undefined,
|
|
41
44
|
execution_id: undefined,
|
|
42
45
|
execution_status: undefined,
|
|
43
46
|
execution_updated_at: undefined,
|
|
44
|
-
}
|
|
47
|
+
};
|
|
48
|
+
return setBlueprintFrontmatterFields(markdown, fields);
|
|
45
49
|
}
|
|
@@ -47,6 +47,27 @@ export interface ApprovalGateEvaluation {
|
|
|
47
47
|
satisfied: boolean;
|
|
48
48
|
strongReviewers: string[];
|
|
49
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Whether to explain a skipped legacy approval leg to the caller.
|
|
52
|
+
*
|
|
53
|
+
* Exported so the decision is unit-testable without standing up a v2 ledger
|
|
54
|
+
* fixture. Two properties matter and neither is obvious from the call site:
|
|
55
|
+
*
|
|
56
|
+
* 1. Never fail silently. Before this message existed a skipped leg reported as a
|
|
57
|
+
* bare "0 approvals" with no reason, which is the most expensive failure mode
|
|
58
|
+
* in this area.
|
|
59
|
+
* 2. Never cry wolf. A post-rollout blueprint that already holds content-bound v2
|
|
60
|
+
* authority and merely still carries a leftover frontmatter entry needs no
|
|
61
|
+
* message: the leg was not required. `validateApprovalGate` turns sink entries
|
|
62
|
+
* into `Review evidence integrity:` WARNINGS on an otherwise-passing audit, so
|
|
63
|
+
* an unconditional note would flag clean strict runs as suspect.
|
|
64
|
+
*/
|
|
65
|
+
export declare function shouldExplainSkippedLegacyLeg(input: {
|
|
66
|
+
readonly legacyLegApplies: boolean;
|
|
67
|
+
readonly includesPlanPurpose: boolean;
|
|
68
|
+
readonly approvals: unknown;
|
|
69
|
+
readonly currentReviewers: readonly string[];
|
|
70
|
+
}): boolean;
|
|
50
71
|
export declare function countDistinctProvenanceBackedApprovals(file: string, approvals: unknown): number;
|
|
51
72
|
export declare function evaluateApprovalGate(file: string, approvals: unknown, options?: {
|
|
52
73
|
readonly purpose?: "plan" | "delivery";
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
1
2
|
import { readFile } from "node:fs/promises";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import matter from "gray-matter";
|
|
@@ -13,7 +14,7 @@ import { resolveBlueprintRoot } from "#utils/blueprint-root";
|
|
|
13
14
|
import { parseBlueprintDocumentRelativePath } from "#utils/document-paths.js";
|
|
14
15
|
import { relativeBlueprintSlug } from "#lifecycle/local.js";
|
|
15
16
|
import { collectV2ReviewProvenance, collectTrackedReviewProvenance, normalizeTrackedReviewApprovals, stableSlugForBlueprintFile, } from "#lifecycle/review-provenance.js";
|
|
16
|
-
import { isBlueprintExemptFromRollout, ROLLOUT_DATE } from "#lifecycle/rollout-anchor.js";
|
|
17
|
+
import { isBlueprintExemptFromRollout, LEGACY_APPROVAL_LEG_ANCHOR_COMMIT, LEGACY_APPROVAL_LEG_ROLLOUT_DATE, ROLLOUT_DATE, } from "#lifecycle/rollout-anchor.js";
|
|
17
18
|
import { MAINTAINER_OVERRIDE_REVIEWER } from "#review/events.js";
|
|
18
19
|
import { isPolicyApprovalVerdict } from "#review/verdict.js";
|
|
19
20
|
function isBlueprintOverview(file) {
|
|
@@ -106,14 +107,99 @@ function approvalReviewerKey(reviewer) {
|
|
|
106
107
|
const parts = normalized.split(/[/:@#]/).filter(Boolean);
|
|
107
108
|
return parts.at(-1) ?? normalized;
|
|
108
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* `created:` as written in the blueprint's own frontmatter.
|
|
112
|
+
*
|
|
113
|
+
* Only consulted on the non-agent-kit fallback path inside
|
|
114
|
+
* {@link isBlueprintExemptFromRollout} (where the anchor commit is absent and
|
|
115
|
+
* cannot answer the question), so the fact that the gated author writes this
|
|
116
|
+
* value does not weaken the gate where it is enforced. Read from disk rather than
|
|
117
|
+
* threaded through {@link evaluateApprovalGate}'s ten call sites: a call site
|
|
118
|
+
* that forgot the parameter would silently fail the leg closed and block
|
|
119
|
+
* promotion on a grandfathered blueprint.
|
|
120
|
+
*/
|
|
121
|
+
function readBlueprintCreated(file) {
|
|
122
|
+
try {
|
|
123
|
+
return matter(readFileSync(file, "utf8")).data.created;
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* True when this blueprint may still satisfy the approval gate through the
|
|
131
|
+
* LEGACY frontmatter `approvals:` leg.
|
|
132
|
+
*
|
|
133
|
+
* That leg carries no content binding: `collectTrackedReviewProvenance` resolves
|
|
134
|
+
* a ledger from `approval.evidence`, matches a `reviews.md` record and requires a
|
|
135
|
+
* tracked artifact, but never reads or hashes the blueprint body — and the gate
|
|
136
|
+
* supplies no `targetHash`, so `matchesApproval`'s binding clause is skipped too.
|
|
137
|
+
* An approval recorded against one plan therefore kept satisfying the gate after
|
|
138
|
+
* the plan was rewritten into something else entirely.
|
|
139
|
+
*
|
|
140
|
+
* The leg is grandfathered rather than deleted because 172 already-landed
|
|
141
|
+
* blueprints resolve their approval through it; failing those would block
|
|
142
|
+
* ordinary work (a completed blueprint touched again in a PR is re-gated). New
|
|
143
|
+
* blueprints get the fail-closed behaviour: their authority must come from the
|
|
144
|
+
* content-addressed v2 `review-events.jsonl` ledger, which `wp review gate` and
|
|
145
|
+
* `wp review override` already write.
|
|
146
|
+
*
|
|
147
|
+
* `approval.targetHash`/`commit`/`rev` do NOT make an approval content-bound —
|
|
148
|
+
* those fields only cross-check the approval against its `reviews.md` record
|
|
149
|
+
* (`matchesApproval`), and neither is ever compared against document content. So
|
|
150
|
+
* presence of a binding-looking field is deliberately not an escape from this
|
|
151
|
+
* check.
|
|
152
|
+
*/
|
|
153
|
+
function legacyApprovalLegIsGrandfathered(file) {
|
|
154
|
+
return isBlueprintExemptFromRollout({
|
|
155
|
+
repoRoot: findGitRoot(file),
|
|
156
|
+
slug: stableSlugForBlueprintFile(file),
|
|
157
|
+
created: readBlueprintCreated(file),
|
|
158
|
+
rolloutDate: LEGACY_APPROVAL_LEG_ROLLOUT_DATE,
|
|
159
|
+
anchorCommit: LEGACY_APPROVAL_LEG_ANCHOR_COMMIT,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
function hasPolicyApprovalEntry(approvals) {
|
|
163
|
+
return normalizeTrackedReviewApprovals(approvals).some((entry) => isPolicyApprovalVerdict(entry.verdict));
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Whether to explain a skipped legacy approval leg to the caller.
|
|
167
|
+
*
|
|
168
|
+
* Exported so the decision is unit-testable without standing up a v2 ledger
|
|
169
|
+
* fixture. Two properties matter and neither is obvious from the call site:
|
|
170
|
+
*
|
|
171
|
+
* 1. Never fail silently. Before this message existed a skipped leg reported as a
|
|
172
|
+
* bare "0 approvals" with no reason, which is the most expensive failure mode
|
|
173
|
+
* in this area.
|
|
174
|
+
* 2. Never cry wolf. A post-rollout blueprint that already holds content-bound v2
|
|
175
|
+
* authority and merely still carries a leftover frontmatter entry needs no
|
|
176
|
+
* message: the leg was not required. `validateApprovalGate` turns sink entries
|
|
177
|
+
* into `Review evidence integrity:` WARNINGS on an otherwise-passing audit, so
|
|
178
|
+
* an unconditional note would flag clean strict runs as suspect.
|
|
179
|
+
*/
|
|
180
|
+
export function shouldExplainSkippedLegacyLeg(input) {
|
|
181
|
+
return (!input.legacyLegApplies &&
|
|
182
|
+
input.includesPlanPurpose &&
|
|
183
|
+
hasPolicyApprovalEntry(input.approvals) &&
|
|
184
|
+
!evaluateApprovalReviewers(input.currentReviewers).satisfied);
|
|
185
|
+
}
|
|
109
186
|
function provenanceBackedApprovalReviewers(file, approvals, options = {}) {
|
|
110
187
|
const purposes = options.purpose
|
|
111
188
|
? [options.purpose]
|
|
112
189
|
: ["plan", "delivery"];
|
|
113
|
-
const
|
|
190
|
+
const current = purposes.flatMap((purpose) => collectV2ReviewProvenance(file, purpose, "HEAD", options.issuesSink).map((record) => record.reviewer));
|
|
191
|
+
const legacyLegApplies = purposes.includes("plan") && legacyApprovalLegIsGrandfathered(file);
|
|
192
|
+
if (shouldExplainSkippedLegacyLeg({
|
|
193
|
+
legacyLegApplies,
|
|
194
|
+
includesPlanPurpose: purposes.includes("plan"),
|
|
195
|
+
approvals,
|
|
196
|
+
currentReviewers: current,
|
|
197
|
+
})) {
|
|
198
|
+
options.issuesSink?.push("legacy frontmatter approval entries are not accepted for this blueprint: the unbound `approvals:` leg is grandfathered only for slugs tracked at the legacy-approval rollout anchor. Record the approval through `wp review gate` so it binds to the reviewed content.");
|
|
199
|
+
}
|
|
200
|
+
const legacy = legacyLegApplies
|
|
114
201
|
? collectTrackedReviewProvenance(file, approvals, options).map((record) => record.reviewer)
|
|
115
202
|
: [];
|
|
116
|
-
const current = purposes.flatMap((purpose) => collectV2ReviewProvenance(file, purpose, "HEAD", options.issuesSink).map((record) => record.reviewer));
|
|
117
203
|
return new Set([...legacy, ...current]);
|
|
118
204
|
}
|
|
119
205
|
export function countDistinctProvenanceBackedApprovals(file, approvals) {
|
|
@@ -108,10 +108,22 @@ export function readBlueprintOverviewAtRef(repoRoot, ref, file) {
|
|
|
108
108
|
}
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
|
-
|
|
111
|
+
/**
|
|
112
|
+
* Whether the working-tree document still represents what `ref` holds, so the
|
|
113
|
+
* ref-side review ledger may be consulted.
|
|
114
|
+
*
|
|
115
|
+
* Every `false` branch records a specific reason in `issues`. Without that, the
|
|
116
|
+
* caller reports "0 approvals" with no `Evidence issues:` suffix and no way to
|
|
117
|
+
* tell a substantive plan edit apart from a missing overview, a path outside the
|
|
118
|
+
* repo, or a failed git probe — an opacity that has cost whole debugging
|
|
119
|
+
* sessions on exactly this class of digest divergence.
|
|
120
|
+
*/
|
|
121
|
+
function workingContentMatchesRef(repoRoot, file, purpose, ref, issues) {
|
|
112
122
|
const relative = path.relative(repoRoot, file);
|
|
113
|
-
if (relative.startsWith("..") || path.isAbsolute(relative))
|
|
123
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
124
|
+
issues?.push(`Review authority at ${ref} not consulted for ${file}: the blueprint path resolves outside the repository root ${repoRoot}.`);
|
|
114
125
|
return false;
|
|
126
|
+
}
|
|
115
127
|
try {
|
|
116
128
|
if (purpose === "plan") {
|
|
117
129
|
// Compare normalized plan content, not raw bytes: promotion rewrites
|
|
@@ -122,10 +134,16 @@ function workingContentMatchesRef(repoRoot, file, purpose, ref) {
|
|
|
122
134
|
// Also resolve by stable slug when the lifecycle path changed (draft→planned)
|
|
123
135
|
// so pre-commit on a rename does not falsely drop plan authority.
|
|
124
136
|
const atRef = readBlueprintOverviewAtRef(repoRoot, ref, file);
|
|
125
|
-
if (!atRef)
|
|
137
|
+
if (!atRef) {
|
|
138
|
+
issues?.push(`Review authority at ${ref} not consulted for ${relative}: no blueprint overview for this slug exists at ${ref}.`);
|
|
126
139
|
return false;
|
|
140
|
+
}
|
|
127
141
|
const working = readFileSync(file);
|
|
128
|
-
|
|
142
|
+
if (normalizePlanSubjectContent(working).equals(normalizePlanSubjectContent(atRef))) {
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
issues?.push(`Review authority at ${ref} not consulted for ${relative}: the working document's normalized plan content differs from ${ref}, so its plan-subject digest differs and no approval recorded against ${ref} can match. Re-run the plan review gate against the current content, or restore the reviewed content.`);
|
|
146
|
+
return false;
|
|
129
147
|
}
|
|
130
148
|
const output = execFileSync("git", ["status", "--porcelain=v1", "--untracked-files=all"], {
|
|
131
149
|
cwd: repoRoot,
|
|
@@ -133,9 +151,13 @@ function workingContentMatchesRef(repoRoot, file, purpose, ref) {
|
|
|
133
151
|
stdio: ["ignore", "pipe", "pipe"],
|
|
134
152
|
timeout: 5_000,
|
|
135
153
|
});
|
|
136
|
-
|
|
154
|
+
if (output.trim().length === 0)
|
|
155
|
+
return true;
|
|
156
|
+
issues?.push(`Review authority at ${ref} not consulted for ${relative}: the working tree is dirty, so a delivery subject at ${ref} does not cover the current content.`);
|
|
157
|
+
return false;
|
|
137
158
|
}
|
|
138
|
-
catch {
|
|
159
|
+
catch (error) {
|
|
160
|
+
issues?.push(`Review authority at ${ref} not consulted for ${relative}: divergence probe failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
139
161
|
return false;
|
|
140
162
|
}
|
|
141
163
|
}
|
|
@@ -224,13 +246,17 @@ function collectWorkingDeliverySubjects(input) {
|
|
|
224
246
|
function collectWorkingV2ReviewProvenance(file, purpose, issues) {
|
|
225
247
|
try {
|
|
226
248
|
const repoRoot = findGitRoot(file);
|
|
227
|
-
if (!repoRoot)
|
|
249
|
+
if (!repoRoot) {
|
|
250
|
+
issues?.push(`Working review authority unavailable for ${file}: no enclosing git repository was found.`);
|
|
228
251
|
return [];
|
|
252
|
+
}
|
|
229
253
|
const slug = stableSlugForBlueprintFile(file);
|
|
230
254
|
const working = readWorkingReviewEvents(file, slug);
|
|
255
|
+
issues?.push(...working.issues);
|
|
256
|
+
// No v2 ledger at all is the normal shape for a blueprint whose authority is
|
|
257
|
+
// a legacy frontmatter approval, so it is not a reason worth reporting.
|
|
231
258
|
if (working.events.length === 0)
|
|
232
259
|
return [];
|
|
233
|
-
issues?.push(...working.issues);
|
|
234
260
|
// Events whose recorded `reviewedCommit` no longer resolves (typically
|
|
235
261
|
// orphaned by a rebase). Collected here rather than mutating
|
|
236
262
|
// `working.compromisedIds`, which is a ReadonlySet by contract, then merged
|
|
@@ -252,7 +278,7 @@ function collectWorkingV2ReviewProvenance(file, purpose, issues) {
|
|
|
252
278
|
unresolvableEventIds,
|
|
253
279
|
issues,
|
|
254
280
|
});
|
|
255
|
-
|
|
281
|
+
const approvals = subjects
|
|
256
282
|
.flatMap((subject) => evaluateReviewEvents(working.events, {
|
|
257
283
|
purpose,
|
|
258
284
|
blueprintSlug: slug,
|
|
@@ -267,6 +293,19 @@ function collectWorkingV2ReviewProvenance(file, purpose, issues) {
|
|
|
267
293
|
artifact: event.artifactPath,
|
|
268
294
|
targetHash: event.subjectDigest,
|
|
269
295
|
}));
|
|
296
|
+
if (approvals.length === 0) {
|
|
297
|
+
// The ledger has events but none of them approves the CURRENT content.
|
|
298
|
+
// Name both sides of the comparison: this is the digest-divergence
|
|
299
|
+
// failure mode, and reporting it as a bare "0 approvals" is what makes it
|
|
300
|
+
// expensive to diagnose.
|
|
301
|
+
const computed = subjects.length === 0
|
|
302
|
+
? "no subject could be computed for the current content"
|
|
303
|
+
: `computed subject digest(s) ${subjects.map((subject) => subject.digest).join(", ")}`;
|
|
304
|
+
issues?.push(`No working-tree approval matched the current ${purpose} subject of ${slug}: ${computed}; the ledger's ${working.events.length} event(s) bind ${[
|
|
305
|
+
...new Set(working.events.map((event) => event.subjectDigest)),
|
|
306
|
+
].join(", ")}.`);
|
|
307
|
+
}
|
|
308
|
+
return approvals;
|
|
270
309
|
}
|
|
271
310
|
catch (error) {
|
|
272
311
|
issues?.push(`Working review authority unavailable for ${file}: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -277,9 +316,23 @@ export function collectV2ReviewProvenance(file, purpose, ref = "HEAD", issues) {
|
|
|
277
316
|
const repoRoot = findGitRoot(file);
|
|
278
317
|
if (!repoRoot)
|
|
279
318
|
return [];
|
|
319
|
+
// Only explain a skipped/unmatched v2 lookup when the blueprint actually has a
|
|
320
|
+
// v2 ledger. A blueprint whose authority is a legacy frontmatter approval has
|
|
321
|
+
// no ledger by design, and narrating the absence for every one of those would
|
|
322
|
+
// bury the one case that matters (a ledger that no longer matches its subject)
|
|
323
|
+
// under warnings that report nothing wrong.
|
|
324
|
+
const explain = existsSync(path.join(path.dirname(file), "review-events.jsonl"))
|
|
325
|
+
? issues
|
|
326
|
+
: undefined;
|
|
280
327
|
const workingApprovals = collectWorkingV2ReviewProvenance(file, purpose, issues);
|
|
281
|
-
if (!workingContentMatchesRef(repoRoot, file, purpose, ref))
|
|
328
|
+
if (!workingContentMatchesRef(repoRoot, file, purpose, ref, explain)) {
|
|
329
|
+
// The ref-side ledger is deliberately skipped here (its approvals bind a
|
|
330
|
+
// subject the working content no longer matches), so the working set is all
|
|
331
|
+
// the authority there is. Say so: an empty result on this path used to be
|
|
332
|
+
// indistinguishable from "there was never any review".
|
|
333
|
+
explain?.push(`Returning ${workingApprovals.length} working-tree approval(s) for ${file} without ref authority from ${ref}.`);
|
|
282
334
|
return workingApprovals;
|
|
335
|
+
}
|
|
283
336
|
try {
|
|
284
337
|
const authority = readReviewAuthorityAtRef(repoRoot, ref, stableSlugForBlueprintFile(file), purpose);
|
|
285
338
|
issues?.push(...authority.issues);
|
|
@@ -25,6 +25,28 @@
|
|
|
25
25
|
export declare const ROLLOUT_ANCHOR_COMMIT = "c6ccb5f31cbecde3be4dbd6d3b7e67918006d22e";
|
|
26
26
|
/** The date that anchor landed, used only by the legacy fallback below. */
|
|
27
27
|
export declare const ROLLOUT_DATE = "2026-07-12";
|
|
28
|
+
/**
|
|
29
|
+
* The commit that closed the legacy frontmatter approval bypass ("fix(review):
|
|
30
|
+
* bind the legacy frontmatter approval leg to the rollout anchor", 2026-07-25).
|
|
31
|
+
*
|
|
32
|
+
* Until that change, a frontmatter `approvals:` entry naming a strong reviewer
|
|
33
|
+
* satisfied the promotion gate with **no content binding whatsoever**: the gate
|
|
34
|
+
* called `evaluateApprovalGate` without a `targetHash`, so `matchesApproval`
|
|
35
|
+
* skipped its binding clause, and `collectTrackedReviewProvenance` never read or
|
|
36
|
+
* hashed the blueprint body at all. Any such blueprint passed the gate no matter
|
|
37
|
+
* what its plan currently said.
|
|
38
|
+
*
|
|
39
|
+
* 172 already-landed blueprints resolve their approval through that leg, so it
|
|
40
|
+
* is grandfathered rather than deleted: slugs tracked at this anchor keep the
|
|
41
|
+
* legacy leg, and every blueprint created afterwards must produce authority
|
|
42
|
+
* through the content-addressed v2 `review-events.jsonl` ledger. Anchor-tree
|
|
43
|
+
* membership is used for the same reason as {@link ROLLOUT_ANCHOR_COMMIT}: no
|
|
44
|
+
* file content can add a slug to a tree that was written earlier, so the
|
|
45
|
+
* grandfather cannot be forged by editing `created:`.
|
|
46
|
+
*/
|
|
47
|
+
export declare const LEGACY_APPROVAL_LEG_ANCHOR_COMMIT = "079be82e39de7099a5bda1261e37598baed8644e";
|
|
48
|
+
/** The date that anchor landed, used only by the non-agent-kit fallback. */
|
|
49
|
+
export declare const LEGACY_APPROVAL_LEG_ROLLOUT_DATE = "2026-07-25";
|
|
28
50
|
/**
|
|
29
51
|
* Outcome of consulting the anchor, cached per (repoRoot, anchorCommit).
|
|
30
52
|
*
|
|
@@ -26,12 +26,42 @@ import { execFileSync } from "node:child_process";
|
|
|
26
26
|
export const ROLLOUT_ANCHOR_COMMIT = "c6ccb5f31cbecde3be4dbd6d3b7e67918006d22e";
|
|
27
27
|
/** The date that anchor landed, used only by the legacy fallback below. */
|
|
28
28
|
export const ROLLOUT_DATE = "2026-07-12";
|
|
29
|
+
/**
|
|
30
|
+
* The commit that closed the legacy frontmatter approval bypass ("fix(review):
|
|
31
|
+
* bind the legacy frontmatter approval leg to the rollout anchor", 2026-07-25).
|
|
32
|
+
*
|
|
33
|
+
* Until that change, a frontmatter `approvals:` entry naming a strong reviewer
|
|
34
|
+
* satisfied the promotion gate with **no content binding whatsoever**: the gate
|
|
35
|
+
* called `evaluateApprovalGate` without a `targetHash`, so `matchesApproval`
|
|
36
|
+
* skipped its binding clause, and `collectTrackedReviewProvenance` never read or
|
|
37
|
+
* hashed the blueprint body at all. Any such blueprint passed the gate no matter
|
|
38
|
+
* what its plan currently said.
|
|
39
|
+
*
|
|
40
|
+
* 172 already-landed blueprints resolve their approval through that leg, so it
|
|
41
|
+
* is grandfathered rather than deleted: slugs tracked at this anchor keep the
|
|
42
|
+
* legacy leg, and every blueprint created afterwards must produce authority
|
|
43
|
+
* through the content-addressed v2 `review-events.jsonl` ledger. Anchor-tree
|
|
44
|
+
* membership is used for the same reason as {@link ROLLOUT_ANCHOR_COMMIT}: no
|
|
45
|
+
* file content can add a slug to a tree that was written earlier, so the
|
|
46
|
+
* grandfather cannot be forged by editing `created:`.
|
|
47
|
+
*/
|
|
48
|
+
export const LEGACY_APPROVAL_LEG_ANCHOR_COMMIT = "079be82e39de7099a5bda1261e37598baed8644e";
|
|
49
|
+
/** The date that anchor landed, used only by the non-agent-kit fallback. */
|
|
50
|
+
export const LEGACY_APPROVAL_LEG_ROLLOUT_DATE = "2026-07-25";
|
|
29
51
|
const anchorSlugCache = new Map();
|
|
52
|
+
/**
|
|
53
|
+
* Every git call here is on the audit/promote hot path, so each carries an
|
|
54
|
+
* explicit deadline (parity with `review-provenance.ts`). A wedged git process
|
|
55
|
+
* must not hang the gate; a timeout surfaces as the same failure the `catch`
|
|
56
|
+
* already handles, which for the two membership probes means fail-closed.
|
|
57
|
+
*/
|
|
58
|
+
const ANCHOR_GIT_TIMEOUT_MS = 5_000;
|
|
30
59
|
function anchorCommitExists(repoRoot, anchorCommit) {
|
|
31
60
|
try {
|
|
32
61
|
execFileSync("git", ["cat-file", "-e", `${anchorCommit}^{commit}`], {
|
|
33
62
|
cwd: repoRoot,
|
|
34
63
|
stdio: ["ignore", "ignore", "ignore"],
|
|
64
|
+
timeout: ANCHOR_GIT_TIMEOUT_MS,
|
|
35
65
|
});
|
|
36
66
|
return true;
|
|
37
67
|
}
|
|
@@ -51,6 +81,7 @@ function isShallowRepository(repoRoot) {
|
|
|
51
81
|
cwd: repoRoot,
|
|
52
82
|
encoding: "utf8",
|
|
53
83
|
stdio: ["ignore", "pipe", "ignore"],
|
|
84
|
+
timeout: ANCHOR_GIT_TIMEOUT_MS,
|
|
54
85
|
});
|
|
55
86
|
// A path that is not a git repository at all answers with a non-zero exit,
|
|
56
87
|
// handled below. Anything else that is not a clean "false" is treated as
|
|
@@ -86,7 +117,12 @@ export function blueprintSlugsAtRolloutAnchor(repoRoot, anchorCommit = ROLLOUT_A
|
|
|
86
117
|
return isShallowRepository(repoRoot) ? { kind: "unreadable" } : { kind: "not-applicable" };
|
|
87
118
|
}
|
|
88
119
|
try {
|
|
89
|
-
const stdout = execFileSync("git", ["ls-tree", "-r", "--name-only", anchorCommit, "blueprints/"], {
|
|
120
|
+
const stdout = execFileSync("git", ["ls-tree", "-r", "--name-only", anchorCommit, "blueprints/"], {
|
|
121
|
+
cwd: repoRoot,
|
|
122
|
+
encoding: "utf8",
|
|
123
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
124
|
+
timeout: ANCHOR_GIT_TIMEOUT_MS,
|
|
125
|
+
});
|
|
90
126
|
const found = new Set();
|
|
91
127
|
for (const line of stdout.split("\n")) {
|
|
92
128
|
const match = /^blueprints\/[^/]+\/(.+)$/u.exec(line.trim());
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single owner of the task-body `**Blocked:** <reason>` line grammar.
|
|
3
|
+
*
|
|
4
|
+
* Both sides of the plan-approval contract read this file: `updateBlockedReason`
|
|
5
|
+
* (`#markdown/helpers.js`) writes the line, and `neutralizeTaskBlockedReason`
|
|
6
|
+
* (`#review/subject.js`) removes it before the plan-subject digest is taken. That
|
|
7
|
+
* pairing is exactly the arrangement the Promotion Gates row grammar already uses
|
|
8
|
+
* (`#trust/gate-row.js`), and for the same reason: when a writer and the
|
|
9
|
+
* normalizer spell the same grammar twice, they drift, the lifecycle stamp
|
|
10
|
+
* survives into the digest, and the plan approval the promotion gates on is
|
|
11
|
+
* silently dismissed.
|
|
12
|
+
*
|
|
13
|
+
* Deliberately dependency-free so any module on either side can import it
|
|
14
|
+
* without risking a cycle.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Regex source for a whole `**Blocked:** …` line, anchored to line bounds.
|
|
18
|
+
*
|
|
19
|
+
* Anchoring is load-bearing in both directions. Blueprint prose legitimately
|
|
20
|
+
* mentions the marker mid-sentence ("add a non-empty **Blocked:** line with the
|
|
21
|
+
* reason") — those mentions are plan substance, and an unanchored pattern would
|
|
22
|
+
* have the writer overwrite them and the normalizer strip them.
|
|
23
|
+
*/
|
|
24
|
+
export declare const BLOCKED_LINE_SOURCE: string;
|
|
25
|
+
/**
|
|
26
|
+
* Build a matcher for the blocked line. Mirrors the `buildTaskHeadingRegex`
|
|
27
|
+
* convention in `./task-heading.js`: callers pick the flags they need, but the
|
|
28
|
+
* pattern itself has exactly one definition.
|
|
29
|
+
*/
|
|
30
|
+
export declare function buildBlockedLineRegex(flags?: string): RegExp;
|
|
31
|
+
/** True when `line` IS a blocked-reason line, not merely a line that mentions one. */
|
|
32
|
+
export declare function isBlockedLine(line: string): boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Serialize the blocked line. The writer-side counterpart to
|
|
35
|
+
* {@link isBlockedLine}: every emitter goes through here so the written form and
|
|
36
|
+
* the parsed form cannot diverge.
|
|
37
|
+
*/
|
|
38
|
+
export declare function formatBlockedLine(reason: string): string;
|
|
39
|
+
/**
|
|
40
|
+
* Read the reason off the first blocked line in a task section, or `undefined`
|
|
41
|
+
* when the section has none.
|
|
42
|
+
*
|
|
43
|
+
* The reader counterpart to {@link formatBlockedLine}. Three parsers used to
|
|
44
|
+
* carry their own unanchored copy of this pattern (`src/blueprint/core/parser.ts`,
|
|
45
|
+
* `src/blueprint/tracked-document/parser.ts`, `src/docs-linter/blueprint-plan.ts`),
|
|
46
|
+
* so a task whose prose merely mentioned the marker was read as genuinely
|
|
47
|
+
* blocked and the lifecycle audit failed the document.
|
|
48
|
+
*/
|
|
49
|
+
export declare function extractBlockedReason(section: string): string | undefined;
|