@webpresso/agent-kit 0.28.0 → 0.29.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +2 -3
- package/README.md +2 -2
- package/bin/_run.js +6 -0
- package/bin/wp +5 -0
- package/catalog/base-kit/.github/actions/setup-webpresso/action.yml.tmpl +21 -0
- package/catalog/base-kit/.github/workflows/{ci.webpresso.yml.tmpl → ci.yml.tmpl} +17 -7
- package/catalog/base-kit/tsconfig.json.tmpl +1 -1
- package/catalog/docs/templates/blueprint.yaml +1 -1
- package/dist/esm/audit/_budgets.d.ts +9 -1
- package/dist/esm/audit/_budgets.js +8 -1
- package/dist/esm/audit/blueprint-db-consistency.js +2 -2
- package/dist/esm/audit/blueprint-lifecycle-sql.d.ts +17 -7
- package/dist/esm/audit/blueprint-lifecycle-sql.js +298 -48
- package/dist/esm/audit/blueprint-readme-drift.d.ts +6 -0
- package/dist/esm/audit/blueprint-readme-drift.js +110 -0
- package/dist/esm/audit/no-first-party-mjs.js +5 -4
- package/dist/esm/audit/package-surface.js +79 -10
- package/dist/esm/audit/repo-guardrails.d.ts +1 -1
- package/dist/esm/audit/repo-guardrails.js +43 -3
- package/dist/esm/audit/tech-debt-cadence.js +2 -3
- package/dist/esm/audit/toolchain-isolation.js +2 -3
- package/dist/esm/blueprint/core/parser.js +3 -2
- package/dist/esm/blueprint/core/schema.d.ts +3 -2
- package/dist/esm/blueprint/core/schema.js +1 -1
- package/dist/esm/blueprint/cross-repo/audit.js +3 -4
- package/dist/esm/blueprint/db/cold-start.js +2 -3
- package/dist/esm/blueprint/db/enums.d.ts +1 -1
- package/dist/esm/blueprint/db/ephemeral-projection.d.ts +25 -0
- package/dist/esm/blueprint/db/ephemeral-projection.js +36 -0
- package/dist/esm/blueprint/db/gc.d.ts +11 -0
- package/dist/esm/blueprint/db/gc.js +55 -0
- package/dist/esm/blueprint/db/ingester.js +39 -1
- package/dist/esm/blueprint/db/migrations/run.js +5 -3
- package/dist/esm/blueprint/db/paths.d.ts +13 -24
- package/dist/esm/blueprint/db/paths.js +25 -33
- package/dist/esm/blueprint/execution/progress-bridge.js +5 -4
- package/dist/esm/blueprint/freshness.d.ts +2 -0
- package/dist/esm/blueprint/freshness.js +3 -1
- package/dist/esm/blueprint/lifecycle/audit.js +6 -6
- package/dist/esm/blueprint/lifecycle/engine.d.ts +1 -1
- package/dist/esm/blueprint/lifecycle/engine.js +13 -9
- package/dist/esm/blueprint/lifecycle/transition-matrix.d.ts +5 -0
- package/dist/esm/blueprint/lifecycle/transition-matrix.js +20 -0
- package/dist/esm/blueprint/markdown/helpers.d.ts +1 -1
- package/dist/esm/blueprint/projection-ready.js +2 -0
- package/dist/esm/blueprint/service/BlueprintService.js +1 -1
- package/dist/esm/blueprint/service/blueprint-records.js +1 -1
- package/dist/esm/blueprint/tracked-document/parser.js +1 -1
- package/dist/esm/blueprint/utils/archive.d.ts +2 -2
- package/dist/esm/blueprint/utils/archive.js +5 -2
- package/dist/esm/blueprint/utils/package-assets.d.ts +13 -0
- package/dist/esm/blueprint/utils/package-assets.js +38 -6
- package/dist/esm/build/normalize-tsconfig-json-exports.d.ts +13 -0
- package/dist/esm/build/normalize-tsconfig-json-exports.js +39 -0
- package/dist/esm/build/package-manifest.js +12 -4
- package/dist/esm/build/release-policy.d.ts +9 -18
- package/dist/esm/build/release-policy.js +10 -19
- package/dist/esm/build/runtime-surface-policy.d.ts +14 -0
- package/dist/esm/build/runtime-surface-policy.js +13 -0
- package/dist/esm/cli/commands/audit-core.d.ts +2 -2
- package/dist/esm/cli/commands/audit.js +7 -3
- package/dist/esm/cli/commands/blueprint/db-commands.js +0 -3
- package/dist/esm/cli/commands/blueprint/mutations.d.ts +3 -2
- package/dist/esm/cli/commands/blueprint/mutations.js +45 -39
- package/dist/esm/cli/commands/blueprint/router-output.js +2 -2
- package/dist/esm/cli/commands/doctor.d.ts +1 -1
- package/dist/esm/cli/commands/doctor.js +4 -5
- package/dist/esm/cli/commands/init/config.d.ts +6 -10
- package/dist/esm/cli/commands/init/config.js +36 -20
- package/dist/esm/cli/commands/init/gitignore-patcher.js +0 -1
- package/dist/esm/cli/commands/init/index.d.ts +8 -1
- package/dist/esm/cli/commands/init/index.js +17 -19
- package/dist/esm/cli/commands/init/package-root.d.ts +20 -0
- package/dist/esm/cli/commands/init/package-root.js +110 -0
- package/dist/esm/cli/commands/init/scaffold-base-kit.js +5 -1
- package/dist/esm/cli/commands/init/scaffolders/agent-hooks/index.d.ts +3 -0
- package/dist/esm/cli/commands/init/scaffolders/agent-hooks/index.js +8 -24
- package/dist/esm/cli/commands/init/scaffolders/agent-kit-global/index.d.ts +9 -0
- package/dist/esm/cli/commands/init/scaffolders/agent-kit-global/index.js +79 -1
- package/dist/esm/cli/commands/init/scaffolders/claude-rules/index.js +2 -12
- package/dist/esm/cli/commands/init/scaffolders/subagents/index.js +2 -12
- package/dist/esm/config/tsconfig/cloudflare.json +1 -1
- package/dist/esm/config/tsconfig/library.json +1 -1
- package/dist/esm/config/tsconfig/react-library.json +3 -2
- package/dist/esm/config/tsconfig/react-router.json +1 -1
- package/dist/esm/dev/restore-dev-links/index.js +3 -4
- package/dist/esm/docs-linter/blueprint-plan.js +46 -4
- package/dist/esm/hooks/check-dev-link/index.js +3 -4
- package/dist/esm/hooks/doctor.d.ts +11 -0
- package/dist/esm/hooks/doctor.js +174 -30
- package/dist/esm/hooks/guard-switch/index.js +3 -5
- package/dist/esm/hooks/post-tool/lint-after-edit.js +4 -5
- package/dist/esm/hooks/pretool-guard/index.js +2 -4
- package/dist/esm/hooks/pretool-guard/runner.js +2 -4
- package/dist/esm/hooks/pretool-guard/validators/forbidden-commands.js +47 -6
- package/dist/esm/hooks/sessionstart/index.js +3 -4
- package/dist/esm/hooks/shared/direct-entrypoint.d.ts +10 -0
- package/dist/esm/hooks/shared/direct-entrypoint.js +21 -0
- package/dist/esm/hooks/stop/qa-changed-files.js +3 -5
- package/dist/esm/hooks/test-quality-check.js +3 -4
- package/dist/esm/mcp/blueprint-server.js +26 -3
- package/dist/esm/mcp/cli.js +2 -6
- package/dist/esm/mcp/server.d.ts +2 -0
- package/dist/esm/mcp/server.js +18 -3
- package/dist/esm/mcp/tools/_shared/audit-kinds.d.ts +1 -1
- package/dist/esm/mcp/tools/_shared/audit-kinds.js +1 -0
- package/dist/esm/mcp/tools/audit.d.ts +2 -1
- package/dist/esm/mcp/tools/audit.js +13 -3
- package/dist/esm/package.json +2 -0
- package/package.json +24 -15
- package/tsconfig/cloudflare.json +1 -1
- package/tsconfig/library.json +1 -1
- package/tsconfig/react-library.json +3 -2
- package/tsconfig/react-router.json +1 -1
- package/dist/esm/blueprint/db/legacy-migration.d.ts +0 -41
- package/dist/esm/blueprint/db/legacy-migration.js +0 -122
|
@@ -1,60 +1,185 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `wp audit blueprint-lifecycle
|
|
3
|
-
*
|
|
2
|
+
* `wp audit blueprint-lifecycle` — the single, deterministic blueprint-lifecycle
|
|
3
|
+
* audit.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* The verdict is a pure function of `markdown@HEAD`: this builds an EPHEMERAL
|
|
6
|
+
* in-memory SQLite projection from the repo's blueprint markdown
|
|
7
|
+
* (`buildEphemeralProjection`), runs the relational checks against it, and
|
|
8
|
+
* discards it. It also runs the structural markdown checks
|
|
9
|
+
* (`auditBlueprintLifecycle` — type / status-vs-folder / `_overview.md` presence /
|
|
10
|
+
* linking-frontmatter) and merges both result sets. No persistent on-disk
|
|
11
|
+
* projection is read, so the audit can never hit a stale/missing/locked DB and
|
|
12
|
+
* is identical across CLI, the `wp_audit` MCP tool, `wp doctor`, and CI.
|
|
7
13
|
*
|
|
8
|
-
*
|
|
14
|
+
* Relational checks (against the in-memory projection):
|
|
9
15
|
* 1. Blueprints with status='in-progress' that have 0 tasks (invalid).
|
|
10
16
|
* 2. Blueprints whose `status` column doesn't match the directory segment
|
|
11
|
-
* derived from `file_path
|
|
17
|
+
* derived from `file_path`.
|
|
12
18
|
* 3. Tasks in state 'in-progress' whose dependencies are not all done.
|
|
13
19
|
* 4. Blueprints with progress_pct < 100 but status='completed'.
|
|
14
20
|
*/
|
|
21
|
+
import { execFileSync } from 'node:child_process';
|
|
22
|
+
import { readFileSync } from 'node:fs';
|
|
15
23
|
import path from 'node:path';
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
// legacy file is moved (and gone) before we count rows. After this call the
|
|
25
|
-
// canonical worktree-scoped path is the single source of truth.
|
|
26
|
-
migrateLegacyAgentDb(cwd);
|
|
27
|
-
// Prefer the canonical worktree-scoped path. If the migration failed because
|
|
28
|
-
// the destination already existed, the warning is already logged; we trust
|
|
29
|
-
// the canonical DB and never read the legacy file in addition, which would
|
|
30
|
-
// double-count rows.
|
|
31
|
-
let dbFile;
|
|
24
|
+
import { getLegalLifecycleTargets, isLegalLifecycleTransition, parseLifecycleBlueprintStatus, } from '#lifecycle/transition-matrix.js';
|
|
25
|
+
import { buildEphemeralProjection } from '#db/ephemeral-projection.js';
|
|
26
|
+
import { loadBudgets } from './_budgets.js';
|
|
27
|
+
/** A task is "terminal" (counts as finished) when it is done OR intentionally dropped. */
|
|
28
|
+
const TERMINAL_TASK_SQL = "('done','dropped')";
|
|
29
|
+
const STALENESS_SCOPE = new Set(['in-progress']);
|
|
30
|
+
const STALENESS_WARNING_PREFIX = '[warn]';
|
|
31
|
+
function isGitHistoryAvailable(cwd) {
|
|
32
32
|
try {
|
|
33
|
-
|
|
33
|
+
execFileSync('git', ['rev-parse', '--show-toplevel'], {
|
|
34
|
+
cwd,
|
|
35
|
+
encoding: 'utf8',
|
|
36
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
37
|
+
timeout: 1_500,
|
|
38
|
+
});
|
|
39
|
+
return true;
|
|
34
40
|
}
|
|
35
41
|
catch {
|
|
36
|
-
|
|
42
|
+
return false;
|
|
37
43
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
44
|
+
}
|
|
45
|
+
function readLastGitTouchIso(cwd, filePath) {
|
|
46
|
+
try {
|
|
47
|
+
const repoRelativePath = path.isAbsolute(filePath) ? path.relative(cwd, filePath) : filePath;
|
|
48
|
+
const out = execFileSync('git', ['log', '-1', '--format=%cI', '--', repoRelativePath], {
|
|
49
|
+
cwd,
|
|
50
|
+
encoding: 'utf8',
|
|
51
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
52
|
+
timeout: 1_500,
|
|
53
|
+
}).trim();
|
|
54
|
+
return out.length > 0 ? out : null;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function ageInDays(isoTimestamp, nowMs) {
|
|
61
|
+
const touchedAtMs = Date.parse(isoTimestamp);
|
|
62
|
+
if (Number.isNaN(touchedAtMs))
|
|
63
|
+
return null;
|
|
64
|
+
return Math.floor((nowMs - touchedAtMs) / 86_400_000);
|
|
65
|
+
}
|
|
66
|
+
function readFrontmatterStatus(markdown) {
|
|
67
|
+
const frontmatterBody = readFrontmatterBody(markdown);
|
|
68
|
+
if (!frontmatterBody)
|
|
69
|
+
return null;
|
|
70
|
+
const statusMatch = frontmatterBody.match(/^status:\s*(.+)$/m);
|
|
71
|
+
if (!statusMatch?.[1])
|
|
72
|
+
return null;
|
|
73
|
+
return statusMatch[1].trim().replace(/^['"]|['"]$/g, '');
|
|
74
|
+
}
|
|
75
|
+
function readFrontmatterBody(markdown) {
|
|
76
|
+
const frontmatterMatch = markdown.match(/^---\n([\s\S]*?)\n---/m);
|
|
77
|
+
return frontmatterMatch?.[1] ?? null;
|
|
78
|
+
}
|
|
79
|
+
function hasHistoricalVerificationGapWaiver(markdown) {
|
|
80
|
+
const frontmatterBody = readFrontmatterBody(markdown);
|
|
81
|
+
if (!frontmatterBody)
|
|
82
|
+
return false;
|
|
83
|
+
return /^historical_verification_gap_waiver:\s*true\s*$/m.test(frontmatterBody);
|
|
84
|
+
}
|
|
85
|
+
function listBlueprintHistoryEntries(cwd, filePath) {
|
|
86
|
+
try {
|
|
87
|
+
const repoRelativePath = path.isAbsolute(filePath) ? path.relative(cwd, filePath) : filePath;
|
|
88
|
+
let trackedPath = repoRelativePath.replace(/\\/g, '/');
|
|
89
|
+
const out = execFileSync('git', ['log', '--follow', '--format=commit:%H', '--name-status', '--', trackedPath], {
|
|
90
|
+
cwd,
|
|
91
|
+
encoding: 'utf8',
|
|
92
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
93
|
+
timeout: 1_500,
|
|
94
|
+
maxBuffer: 1024 * 1024,
|
|
95
|
+
});
|
|
96
|
+
const history = [];
|
|
97
|
+
let currentRevision = null;
|
|
98
|
+
let currentPathAtRevision = null;
|
|
99
|
+
let nextTrackedPath = trackedPath;
|
|
100
|
+
const flushEntry = () => {
|
|
101
|
+
if (!currentRevision || !currentPathAtRevision)
|
|
102
|
+
return;
|
|
103
|
+
history.push({ revision: currentRevision, filePath: currentPathAtRevision });
|
|
104
|
+
trackedPath = nextTrackedPath;
|
|
105
|
+
};
|
|
106
|
+
for (const rawLine of out.split('\n')) {
|
|
107
|
+
const line = rawLine.trim();
|
|
108
|
+
if (!line)
|
|
109
|
+
continue;
|
|
110
|
+
if (line.startsWith('commit:')) {
|
|
111
|
+
flushEntry();
|
|
112
|
+
currentRevision = line.slice('commit:'.length).trim();
|
|
113
|
+
currentPathAtRevision = trackedPath;
|
|
114
|
+
nextTrackedPath = trackedPath;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const parts = line.split('\t');
|
|
118
|
+
const status = parts[0]?.trim() ?? '';
|
|
119
|
+
if (!status.startsWith('R'))
|
|
120
|
+
continue;
|
|
121
|
+
const oldPath = parts[1]?.trim().replace(/\\/g, '/');
|
|
122
|
+
const newPath = parts[2]?.trim().replace(/\\/g, '/');
|
|
123
|
+
if (oldPath && newPath && newPath === currentPathAtRevision) {
|
|
124
|
+
nextTrackedPath = oldPath;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
flushEntry();
|
|
128
|
+
return history;
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return [];
|
|
42
132
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
133
|
+
}
|
|
134
|
+
function readHistoricalFile(cwd, revision, filePath) {
|
|
135
|
+
try {
|
|
136
|
+
return execFileSync('git', ['show', `${revision}:${filePath}`], {
|
|
137
|
+
cwd,
|
|
138
|
+
encoding: 'utf8',
|
|
139
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
140
|
+
timeout: 1_500,
|
|
141
|
+
maxBuffer: 1024 * 1024,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function readPreviousLifecycleStatusFromGit(cwd, filePath, currentStatus) {
|
|
149
|
+
const history = listBlueprintHistoryEntries(cwd, filePath);
|
|
150
|
+
if (history.length < 2)
|
|
151
|
+
return null;
|
|
152
|
+
for (const entry of history.slice(1)) {
|
|
153
|
+
const markdown = readHistoricalFile(cwd, entry.revision, entry.filePath);
|
|
154
|
+
if (!markdown)
|
|
155
|
+
continue;
|
|
156
|
+
const status = readFrontmatterStatus(markdown);
|
|
157
|
+
if (!status || status === currentStatus)
|
|
158
|
+
continue;
|
|
159
|
+
return status;
|
|
160
|
+
}
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
export async function auditBlueprintLifecycleSql(cwd = process.cwd(), options = {}) {
|
|
164
|
+
const budgets = loadBudgets(cwd);
|
|
165
|
+
const wipInProgressMax = budgets['blueprint-wip-in-progress-max'].max ?? 3;
|
|
166
|
+
const staleInProgressDays = budgets['blueprint-stale-in-progress-days'].max_days ?? 14;
|
|
167
|
+
// Structural markdown checks (type / status-vs-folder / _overview / linking /
|
|
168
|
+
// optional .omx-plan handoff governance). Run unconditionally and merged —
|
|
169
|
+
// this is NOT a fallback. Dynamic import keeps the heavy guardrails module off
|
|
170
|
+
// the hook-runtime hot path until the audit runs.
|
|
171
|
+
const { auditBlueprintLifecycle } = await import('./repo-guardrails.js');
|
|
172
|
+
const structural = auditBlueprintLifecycle(cwd, options);
|
|
173
|
+
const violations = [...structural.violations];
|
|
174
|
+
const advisoryViolations = [];
|
|
175
|
+
let checked = structural.checked;
|
|
176
|
+
const titleNotices = [];
|
|
177
|
+
const conn = await buildEphemeralProjection(cwd);
|
|
178
|
+
const { db } = conn;
|
|
47
179
|
try {
|
|
48
180
|
const allBlueprints = db
|
|
49
181
|
.prepare('SELECT slug, status, file_path, progress_pct FROM blueprints')
|
|
50
182
|
.all();
|
|
51
|
-
if (allBlueprints.length === 0) {
|
|
52
|
-
const { auditBlueprintLifecycle } = await import('./repo-guardrails.js');
|
|
53
|
-
const markdownAudit = auditBlueprintLifecycle(cwd);
|
|
54
|
-
if (markdownAudit.checked > 0) {
|
|
55
|
-
return markdownAudit;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
183
|
// -----------------------------------------------------------------------
|
|
59
184
|
// 1. in-progress blueprints with 0 tasks
|
|
60
185
|
// -----------------------------------------------------------------------
|
|
@@ -75,12 +200,10 @@ export async function auditBlueprintLifecycleSql(cwd) {
|
|
|
75
200
|
}
|
|
76
201
|
// -----------------------------------------------------------------------
|
|
77
202
|
// 2. status/directory mismatch
|
|
78
|
-
// Derive the directory segment from the file_path and compare to status.
|
|
79
203
|
// Blueprint file_path convention: blueprints/<status>/<slug>/_overview.md
|
|
80
204
|
// -----------------------------------------------------------------------
|
|
81
205
|
checked += allBlueprints.length;
|
|
82
206
|
for (const row of allBlueprints) {
|
|
83
|
-
// Derive directory status from the path: second segment after 'blueprints/'
|
|
84
207
|
const segments = row.file_path.replace(/\\/g, '/').split('/');
|
|
85
208
|
const blueprintsIdx = segments.lastIndexOf('blueprints');
|
|
86
209
|
const dirStatus = blueprintsIdx >= 0 ? segments[blueprintsIdx + 1] : null;
|
|
@@ -117,27 +240,154 @@ export async function auditBlueprintLifecycleSql(cwd) {
|
|
|
117
240
|
// -----------------------------------------------------------------------
|
|
118
241
|
const incompleteCompleted = db
|
|
119
242
|
.prepare(`SELECT slug, file_path, progress_pct
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
243
|
+
FROM blueprints
|
|
244
|
+
WHERE status = 'completed'
|
|
245
|
+
AND progress_pct IS NOT NULL
|
|
246
|
+
AND progress_pct < 100`)
|
|
124
247
|
.all();
|
|
125
248
|
checked += incompleteCompleted.length;
|
|
126
249
|
for (const row of incompleteCompleted) {
|
|
250
|
+
const hasNonTerminalTask = db
|
|
251
|
+
.prepare(`SELECT COUNT(*) AS open_tasks
|
|
252
|
+
FROM tasks
|
|
253
|
+
WHERE blueprint_slug = ?
|
|
254
|
+
AND status NOT IN ${TERMINAL_TASK_SQL}`)
|
|
255
|
+
.get(row.slug);
|
|
256
|
+
if ((hasNonTerminalTask?.open_tasks ?? 0) === 0)
|
|
257
|
+
continue;
|
|
127
258
|
violations.push({
|
|
128
259
|
file: row.file_path,
|
|
129
260
|
message: `Blueprint '${row.slug}' is marked completed but progress_pct is ${row.progress_pct}% (expected 100)`,
|
|
130
261
|
});
|
|
131
262
|
}
|
|
263
|
+
// -----------------------------------------------------------------------
|
|
264
|
+
// 5. in-progress blueprints whose tasks are ALL terminal (done|dropped)
|
|
265
|
+
// — finished work left in the in-progress lane. terminal = done ∪ dropped
|
|
266
|
+
// so a de-scoped task doesn't keep a finished blueprint pinned forever.
|
|
267
|
+
// -----------------------------------------------------------------------
|
|
268
|
+
const allTerminalInProgress = db
|
|
269
|
+
.prepare(`SELECT b.slug, b.file_path, COUNT(t.id) AS total
|
|
270
|
+
FROM blueprints b
|
|
271
|
+
JOIN tasks t ON t.blueprint_slug = b.slug
|
|
272
|
+
WHERE b.status = 'in-progress'
|
|
273
|
+
GROUP BY b.slug, b.file_path
|
|
274
|
+
HAVING COUNT(t.id) > 0
|
|
275
|
+
AND SUM(CASE WHEN t.status IN ${TERMINAL_TASK_SQL} THEN 1 ELSE 0 END) = COUNT(t.id)`)
|
|
276
|
+
.all();
|
|
277
|
+
checked += allTerminalInProgress.length;
|
|
278
|
+
for (const row of allTerminalInProgress) {
|
|
279
|
+
violations.push({
|
|
280
|
+
file: row.file_path,
|
|
281
|
+
message: `Blueprint '${row.slug}' has all ${row.total} tasks done/dropped but is still in 'in-progress/' — move it to completed/ or reopen a task`,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
// -----------------------------------------------------------------------
|
|
285
|
+
// 6. completed blueprints with a non-terminal task (status untruthful)
|
|
286
|
+
// -----------------------------------------------------------------------
|
|
287
|
+
const completedWithOpenTasks = db
|
|
288
|
+
.prepare(`SELECT b.slug, b.file_path
|
|
289
|
+
FROM blueprints b
|
|
290
|
+
WHERE b.status = 'completed'
|
|
291
|
+
AND EXISTS (
|
|
292
|
+
SELECT 1 FROM tasks t
|
|
293
|
+
WHERE t.blueprint_slug = b.slug
|
|
294
|
+
AND t.status NOT IN ${TERMINAL_TASK_SQL}
|
|
295
|
+
)`)
|
|
296
|
+
.all();
|
|
297
|
+
checked += completedWithOpenTasks.length;
|
|
298
|
+
for (const row of completedWithOpenTasks) {
|
|
299
|
+
violations.push({
|
|
300
|
+
file: row.file_path,
|
|
301
|
+
message: `Blueprint '${row.slug}' is marked completed but has tasks that are not done/dropped`,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
// -----------------------------------------------------------------------
|
|
305
|
+
// 7. WIP limit — at most the configured max blueprints in the in-progress lane
|
|
306
|
+
// -----------------------------------------------------------------------
|
|
307
|
+
const inProgressCountRows = db
|
|
308
|
+
.prepare(`SELECT COUNT(*) AS n FROM blueprints WHERE status = 'in-progress'`)
|
|
309
|
+
.all();
|
|
310
|
+
const inProgressCount = inProgressCountRows[0]?.n ?? 0;
|
|
311
|
+
checked += 1;
|
|
312
|
+
if (inProgressCount > wipInProgressMax) {
|
|
313
|
+
violations.push({
|
|
314
|
+
message: `${inProgressCount} blueprints are in-progress — the lane limit is ${wipInProgressMax} (budget: blueprint-wip-in-progress-max); finish or park some before starting more`,
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
// -----------------------------------------------------------------------
|
|
318
|
+
// 8. Staleness — warn (do not fail) when an in-progress blueprint has not
|
|
319
|
+
// been touched in git within the configured day budget.
|
|
320
|
+
// -----------------------------------------------------------------------
|
|
321
|
+
const staleCandidates = allBlueprints.filter((row) => STALENESS_SCOPE.has(row.status));
|
|
322
|
+
checked += staleCandidates.length;
|
|
323
|
+
if (staleCandidates.length > 0) {
|
|
324
|
+
if (isGitHistoryAvailable(cwd)) {
|
|
325
|
+
const nowMs = Date.now();
|
|
326
|
+
for (const row of staleCandidates) {
|
|
327
|
+
const lastTouchIso = readLastGitTouchIso(cwd, row.file_path);
|
|
328
|
+
if (lastTouchIso === null)
|
|
329
|
+
continue;
|
|
330
|
+
const ageDays = ageInDays(lastTouchIso, nowMs);
|
|
331
|
+
if (ageDays === null || ageDays <= staleInProgressDays)
|
|
332
|
+
continue;
|
|
333
|
+
advisoryViolations.push({
|
|
334
|
+
file: row.file_path,
|
|
335
|
+
message: `${STALENESS_WARNING_PREFIX} Blueprint '${row.slug}' is stale: last git touch was ` +
|
|
336
|
+
`${lastTouchIso.slice(0, 10)} (${ageDays} days ago), exceeding ` +
|
|
337
|
+
`blueprint-stale-in-progress-days=${staleInProgressDays}`,
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
titleNotices.push('staleness check skipped outside git');
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
// -----------------------------------------------------------------------
|
|
346
|
+
// 9. Transition legality — best effort, based on previous lifecycle status
|
|
347
|
+
// observed in git history. Missing history fails open by design.
|
|
348
|
+
// -----------------------------------------------------------------------
|
|
349
|
+
checked += allBlueprints.length;
|
|
350
|
+
if (allBlueprints.length === 0) {
|
|
351
|
+
// Nothing to reconcile against history, so suppress the outside-git notice.
|
|
352
|
+
}
|
|
353
|
+
else if (isGitHistoryAvailable(cwd)) {
|
|
354
|
+
for (const row of allBlueprints) {
|
|
355
|
+
const currentMarkdown = readFileSync(row.file_path, 'utf8');
|
|
356
|
+
if (hasHistoricalVerificationGapWaiver(currentMarkdown))
|
|
357
|
+
continue;
|
|
358
|
+
const currentStatus = parseLifecycleBlueprintStatus(row.status);
|
|
359
|
+
if (!currentStatus)
|
|
360
|
+
continue;
|
|
361
|
+
const previousRaw = readPreviousLifecycleStatusFromGit(cwd, row.file_path, currentStatus);
|
|
362
|
+
if (!previousRaw)
|
|
363
|
+
continue;
|
|
364
|
+
const previousStatus = parseLifecycleBlueprintStatus(previousRaw);
|
|
365
|
+
if (!previousStatus)
|
|
366
|
+
continue;
|
|
367
|
+
if (isLegalLifecycleTransition(previousStatus, currentStatus))
|
|
368
|
+
continue;
|
|
369
|
+
violations.push({
|
|
370
|
+
file: row.file_path,
|
|
371
|
+
message: `Blueprint '${row.slug}' moved from '${previousStatus}' to '${currentStatus}', which is illegal; ` +
|
|
372
|
+
`legal targets from '${previousStatus}' are: ${getLegalLifecycleTargets(previousStatus).join(', ') || '(none)'}`,
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
titleNotices.push('transition history check skipped outside git');
|
|
378
|
+
}
|
|
379
|
+
const title = titleNotices.length === 0
|
|
380
|
+
? 'Blueprint lifecycle'
|
|
381
|
+
: `Blueprint lifecycle — ${titleNotices.join('; ')}`;
|
|
132
382
|
return {
|
|
133
383
|
ok: violations.length === 0,
|
|
134
|
-
title
|
|
384
|
+
title,
|
|
135
385
|
checked,
|
|
136
|
-
violations,
|
|
386
|
+
violations: [...violations, ...advisoryViolations],
|
|
137
387
|
};
|
|
138
388
|
}
|
|
139
389
|
finally {
|
|
140
|
-
|
|
390
|
+
conn.close();
|
|
141
391
|
}
|
|
142
392
|
}
|
|
143
393
|
//# sourceMappingURL=blueprint-lifecycle-sql.js.map
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { RepoAuditResult } from './repo-guardrails.js';
|
|
2
|
+
export interface BlueprintReadmeDriftOptions {
|
|
3
|
+
fix?: boolean;
|
|
4
|
+
}
|
|
5
|
+
export declare function auditBlueprintReadmeDrift(cwd?: string, options?: BlueprintReadmeDriftOptions): RepoAuditResult;
|
|
6
|
+
//# sourceMappingURL=blueprint-readme-drift.d.ts.map
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { scanBlueprintDirectory } from '#service/scanner.js';
|
|
4
|
+
import { parseBlueprintDocumentRelativePath } from '#utils/document-paths.js';
|
|
5
|
+
const README_RELATIVE_PATH = 'blueprints/README.md';
|
|
6
|
+
const BEGIN_MARKER = '<!-- BEGIN: blueprint-index (generated by `wp audit blueprint-readme-drift --fix`) -->';
|
|
7
|
+
const END_MARKER = '<!-- END: blueprint-index -->';
|
|
8
|
+
const README_INSERT_HEADING = '## Authoring';
|
|
9
|
+
const README_STALE_MESSAGE = "blueprints/README.md index block is stale — run 'wp audit blueprint-readme-drift --fix'";
|
|
10
|
+
const STATE_ROWS = [
|
|
11
|
+
['draft', 'early-stage sketches. Expect churn; move to `planned/` once scoped.'],
|
|
12
|
+
['planned', 'committed-to specs, ready to pick up.'],
|
|
13
|
+
['in-progress', 'actively being executed. At most 3 active blueprints per lane.'],
|
|
14
|
+
['completed', 'execution finished and verified. Kept for reference.'],
|
|
15
|
+
['parked', "intentionally paused. Include a reason in the spec's frontmatter."],
|
|
16
|
+
['archived', 'superseded or abandoned. Not deleted — the record matters.'],
|
|
17
|
+
];
|
|
18
|
+
function initialCounts() {
|
|
19
|
+
return {
|
|
20
|
+
draft: 0,
|
|
21
|
+
planned: 0,
|
|
22
|
+
'in-progress': 0,
|
|
23
|
+
completed: 0,
|
|
24
|
+
parked: 0,
|
|
25
|
+
archived: 0,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function countBlueprintsByState(cwd) {
|
|
29
|
+
const counts = initialCounts();
|
|
30
|
+
const blueprintsRoot = path.join(cwd, 'blueprints');
|
|
31
|
+
if (!existsSync(blueprintsRoot))
|
|
32
|
+
return counts;
|
|
33
|
+
const scanned = scanBlueprintDirectory({
|
|
34
|
+
baseDir: blueprintsRoot,
|
|
35
|
+
includeSpecialFolders: true,
|
|
36
|
+
});
|
|
37
|
+
for (const blueprint of scanned) {
|
|
38
|
+
const relativePath = path.relative(blueprintsRoot, blueprint.path).replace(/\\/g, '/');
|
|
39
|
+
const parsed = parseBlueprintDocumentRelativePath(relativePath);
|
|
40
|
+
if (!parsed)
|
|
41
|
+
continue;
|
|
42
|
+
counts[parsed.state] += 1;
|
|
43
|
+
}
|
|
44
|
+
return counts;
|
|
45
|
+
}
|
|
46
|
+
function renderGeneratedBlock(counts) {
|
|
47
|
+
const lines = [
|
|
48
|
+
BEGIN_MARKER,
|
|
49
|
+
'| State | Count | Description |',
|
|
50
|
+
'| ----- | ----: | ----------- |',
|
|
51
|
+
...STATE_ROWS.map(([state, description]) => `| \`${state}/\` | ${counts[state]} | ${description} |`),
|
|
52
|
+
END_MARKER,
|
|
53
|
+
];
|
|
54
|
+
return `${lines.join('\n')}\n`;
|
|
55
|
+
}
|
|
56
|
+
function replaceExistingBlock(markdown, block) {
|
|
57
|
+
const start = markdown.indexOf(BEGIN_MARKER);
|
|
58
|
+
const end = markdown.indexOf(END_MARKER);
|
|
59
|
+
if (start >= 0 && end >= start) {
|
|
60
|
+
const afterEnd = end + END_MARKER.length;
|
|
61
|
+
return `${markdown.slice(0, start)}${block}${markdown.slice(afterEnd)}`;
|
|
62
|
+
}
|
|
63
|
+
const insertAt = markdown.indexOf(README_INSERT_HEADING);
|
|
64
|
+
if (insertAt >= 0) {
|
|
65
|
+
return `${markdown.slice(0, insertAt).replace(/\n*$/, '\n\n')}${block}${markdown.slice(insertAt)}`;
|
|
66
|
+
}
|
|
67
|
+
return `${markdown.replace(/\s*$/, '\n\n')}${block}`;
|
|
68
|
+
}
|
|
69
|
+
function extractExistingBlock(markdown) {
|
|
70
|
+
const start = markdown.indexOf(BEGIN_MARKER);
|
|
71
|
+
const end = markdown.indexOf(END_MARKER);
|
|
72
|
+
if (start < 0 || end < start)
|
|
73
|
+
return null;
|
|
74
|
+
return markdown.slice(start, end + END_MARKER.length);
|
|
75
|
+
}
|
|
76
|
+
export function auditBlueprintReadmeDrift(cwd = process.cwd(), options = {}) {
|
|
77
|
+
const readmePath = path.join(cwd, README_RELATIVE_PATH);
|
|
78
|
+
if (!existsSync(readmePath)) {
|
|
79
|
+
return {
|
|
80
|
+
ok: false,
|
|
81
|
+
title: 'Blueprint README drift',
|
|
82
|
+
checked: 1,
|
|
83
|
+
violations: [{ file: README_RELATIVE_PATH, message: `${README_RELATIVE_PATH} is missing` }],
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
const current = readFileSync(readmePath, 'utf8');
|
|
87
|
+
const expectedBlock = renderGeneratedBlock(countBlueprintsByState(cwd));
|
|
88
|
+
const currentBlock = extractExistingBlock(current);
|
|
89
|
+
const updated = replaceExistingBlock(current, expectedBlock);
|
|
90
|
+
const isInSync = currentBlock !== null && currentBlock.trimEnd() === expectedBlock.trimEnd();
|
|
91
|
+
if (!isInSync && options.fix) {
|
|
92
|
+
mkdirSync(path.dirname(readmePath), { recursive: true });
|
|
93
|
+
writeFileSync(readmePath, updated, 'utf8');
|
|
94
|
+
return {
|
|
95
|
+
ok: true,
|
|
96
|
+
title: 'Blueprint README drift',
|
|
97
|
+
checked: 1,
|
|
98
|
+
violations: [],
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
ok: isInSync,
|
|
103
|
+
title: 'Blueprint README drift',
|
|
104
|
+
checked: 1,
|
|
105
|
+
violations: isInSync
|
|
106
|
+
? []
|
|
107
|
+
: [{ file: README_RELATIVE_PATH, message: README_STALE_MESSAGE }],
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=blueprint-readme-drift.js.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process';
|
|
2
|
-
import { existsSync } from 'node:fs';
|
|
2
|
+
import { existsSync, realpathSync } from 'node:fs';
|
|
3
3
|
import { join, resolve } from 'node:path';
|
|
4
4
|
const IGNORED_PATH_PREFIXES = [
|
|
5
5
|
'node_modules',
|
|
@@ -34,10 +34,10 @@ function fail(root, message) {
|
|
|
34
34
|
};
|
|
35
35
|
}
|
|
36
36
|
function resolveCanonicalRepoRoot(rootDirectory) {
|
|
37
|
-
const requestedRoot = resolve(rootDirectory);
|
|
37
|
+
const requestedRoot = realpathSync.native(resolve(rootDirectory));
|
|
38
38
|
let gitRoot;
|
|
39
39
|
try {
|
|
40
|
-
gitRoot =
|
|
40
|
+
gitRoot = realpathSync.native(execFileSync('git', ['rev-parse', '--show-toplevel'], {
|
|
41
41
|
cwd: requestedRoot,
|
|
42
42
|
encoding: 'utf8',
|
|
43
43
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
@@ -63,7 +63,8 @@ function listTrackedFiles(root) {
|
|
|
63
63
|
return output
|
|
64
64
|
.split('\n')
|
|
65
65
|
.map((line) => line.trim())
|
|
66
|
-
.filter(Boolean)
|
|
66
|
+
.filter(Boolean)
|
|
67
|
+
.filter((relativePath) => existsSync(join(root, relativePath)));
|
|
67
68
|
}
|
|
68
69
|
export function auditNoFirstPartyMjs(rootDirectory = process.cwd()) {
|
|
69
70
|
const canonical = resolveCanonicalRepoRoot(rootDirectory);
|