@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.
Files changed (117) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +2 -3
  3. package/README.md +2 -2
  4. package/bin/_run.js +6 -0
  5. package/bin/wp +5 -0
  6. package/catalog/base-kit/.github/actions/setup-webpresso/action.yml.tmpl +21 -0
  7. package/catalog/base-kit/.github/workflows/{ci.webpresso.yml.tmpl → ci.yml.tmpl} +17 -7
  8. package/catalog/base-kit/tsconfig.json.tmpl +1 -1
  9. package/catalog/docs/templates/blueprint.yaml +1 -1
  10. package/dist/esm/audit/_budgets.d.ts +9 -1
  11. package/dist/esm/audit/_budgets.js +8 -1
  12. package/dist/esm/audit/blueprint-db-consistency.js +2 -2
  13. package/dist/esm/audit/blueprint-lifecycle-sql.d.ts +17 -7
  14. package/dist/esm/audit/blueprint-lifecycle-sql.js +298 -48
  15. package/dist/esm/audit/blueprint-readme-drift.d.ts +6 -0
  16. package/dist/esm/audit/blueprint-readme-drift.js +110 -0
  17. package/dist/esm/audit/no-first-party-mjs.js +5 -4
  18. package/dist/esm/audit/package-surface.js +79 -10
  19. package/dist/esm/audit/repo-guardrails.d.ts +1 -1
  20. package/dist/esm/audit/repo-guardrails.js +43 -3
  21. package/dist/esm/audit/tech-debt-cadence.js +2 -3
  22. package/dist/esm/audit/toolchain-isolation.js +2 -3
  23. package/dist/esm/blueprint/core/parser.js +3 -2
  24. package/dist/esm/blueprint/core/schema.d.ts +3 -2
  25. package/dist/esm/blueprint/core/schema.js +1 -1
  26. package/dist/esm/blueprint/cross-repo/audit.js +3 -4
  27. package/dist/esm/blueprint/db/cold-start.js +2 -3
  28. package/dist/esm/blueprint/db/enums.d.ts +1 -1
  29. package/dist/esm/blueprint/db/ephemeral-projection.d.ts +25 -0
  30. package/dist/esm/blueprint/db/ephemeral-projection.js +36 -0
  31. package/dist/esm/blueprint/db/gc.d.ts +11 -0
  32. package/dist/esm/blueprint/db/gc.js +55 -0
  33. package/dist/esm/blueprint/db/ingester.js +39 -1
  34. package/dist/esm/blueprint/db/migrations/run.js +5 -3
  35. package/dist/esm/blueprint/db/paths.d.ts +13 -24
  36. package/dist/esm/blueprint/db/paths.js +25 -33
  37. package/dist/esm/blueprint/execution/progress-bridge.js +5 -4
  38. package/dist/esm/blueprint/freshness.d.ts +2 -0
  39. package/dist/esm/blueprint/freshness.js +3 -1
  40. package/dist/esm/blueprint/lifecycle/audit.js +6 -6
  41. package/dist/esm/blueprint/lifecycle/engine.d.ts +1 -1
  42. package/dist/esm/blueprint/lifecycle/engine.js +13 -9
  43. package/dist/esm/blueprint/lifecycle/transition-matrix.d.ts +5 -0
  44. package/dist/esm/blueprint/lifecycle/transition-matrix.js +20 -0
  45. package/dist/esm/blueprint/markdown/helpers.d.ts +1 -1
  46. package/dist/esm/blueprint/projection-ready.js +2 -0
  47. package/dist/esm/blueprint/service/BlueprintService.js +1 -1
  48. package/dist/esm/blueprint/service/blueprint-records.js +1 -1
  49. package/dist/esm/blueprint/tracked-document/parser.js +1 -1
  50. package/dist/esm/blueprint/utils/archive.d.ts +2 -2
  51. package/dist/esm/blueprint/utils/archive.js +5 -2
  52. package/dist/esm/blueprint/utils/package-assets.d.ts +13 -0
  53. package/dist/esm/blueprint/utils/package-assets.js +38 -6
  54. package/dist/esm/build/normalize-tsconfig-json-exports.d.ts +13 -0
  55. package/dist/esm/build/normalize-tsconfig-json-exports.js +39 -0
  56. package/dist/esm/build/package-manifest.js +12 -4
  57. package/dist/esm/build/release-policy.d.ts +9 -18
  58. package/dist/esm/build/release-policy.js +10 -19
  59. package/dist/esm/build/runtime-surface-policy.d.ts +14 -0
  60. package/dist/esm/build/runtime-surface-policy.js +13 -0
  61. package/dist/esm/cli/commands/audit-core.d.ts +2 -2
  62. package/dist/esm/cli/commands/audit.js +7 -3
  63. package/dist/esm/cli/commands/blueprint/db-commands.js +0 -3
  64. package/dist/esm/cli/commands/blueprint/mutations.d.ts +3 -2
  65. package/dist/esm/cli/commands/blueprint/mutations.js +45 -39
  66. package/dist/esm/cli/commands/blueprint/router-output.js +2 -2
  67. package/dist/esm/cli/commands/doctor.d.ts +1 -1
  68. package/dist/esm/cli/commands/doctor.js +4 -5
  69. package/dist/esm/cli/commands/init/config.d.ts +6 -10
  70. package/dist/esm/cli/commands/init/config.js +36 -20
  71. package/dist/esm/cli/commands/init/gitignore-patcher.js +0 -1
  72. package/dist/esm/cli/commands/init/index.d.ts +8 -1
  73. package/dist/esm/cli/commands/init/index.js +17 -19
  74. package/dist/esm/cli/commands/init/package-root.d.ts +20 -0
  75. package/dist/esm/cli/commands/init/package-root.js +110 -0
  76. package/dist/esm/cli/commands/init/scaffold-base-kit.js +5 -1
  77. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/index.d.ts +3 -0
  78. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/index.js +8 -24
  79. package/dist/esm/cli/commands/init/scaffolders/agent-kit-global/index.d.ts +9 -0
  80. package/dist/esm/cli/commands/init/scaffolders/agent-kit-global/index.js +79 -1
  81. package/dist/esm/cli/commands/init/scaffolders/claude-rules/index.js +2 -12
  82. package/dist/esm/cli/commands/init/scaffolders/subagents/index.js +2 -12
  83. package/dist/esm/config/tsconfig/cloudflare.json +1 -1
  84. package/dist/esm/config/tsconfig/library.json +1 -1
  85. package/dist/esm/config/tsconfig/react-library.json +3 -2
  86. package/dist/esm/config/tsconfig/react-router.json +1 -1
  87. package/dist/esm/dev/restore-dev-links/index.js +3 -4
  88. package/dist/esm/docs-linter/blueprint-plan.js +46 -4
  89. package/dist/esm/hooks/check-dev-link/index.js +3 -4
  90. package/dist/esm/hooks/doctor.d.ts +11 -0
  91. package/dist/esm/hooks/doctor.js +174 -30
  92. package/dist/esm/hooks/guard-switch/index.js +3 -5
  93. package/dist/esm/hooks/post-tool/lint-after-edit.js +4 -5
  94. package/dist/esm/hooks/pretool-guard/index.js +2 -4
  95. package/dist/esm/hooks/pretool-guard/runner.js +2 -4
  96. package/dist/esm/hooks/pretool-guard/validators/forbidden-commands.js +47 -6
  97. package/dist/esm/hooks/sessionstart/index.js +3 -4
  98. package/dist/esm/hooks/shared/direct-entrypoint.d.ts +10 -0
  99. package/dist/esm/hooks/shared/direct-entrypoint.js +21 -0
  100. package/dist/esm/hooks/stop/qa-changed-files.js +3 -5
  101. package/dist/esm/hooks/test-quality-check.js +3 -4
  102. package/dist/esm/mcp/blueprint-server.js +26 -3
  103. package/dist/esm/mcp/cli.js +2 -6
  104. package/dist/esm/mcp/server.d.ts +2 -0
  105. package/dist/esm/mcp/server.js +18 -3
  106. package/dist/esm/mcp/tools/_shared/audit-kinds.d.ts +1 -1
  107. package/dist/esm/mcp/tools/_shared/audit-kinds.js +1 -0
  108. package/dist/esm/mcp/tools/audit.d.ts +2 -1
  109. package/dist/esm/mcp/tools/audit.js +13 -3
  110. package/dist/esm/package.json +2 -0
  111. package/package.json +24 -15
  112. package/tsconfig/cloudflare.json +1 -1
  113. package/tsconfig/library.json +1 -1
  114. package/tsconfig/react-library.json +3 -2
  115. package/tsconfig/react-router.json +1 -1
  116. package/dist/esm/blueprint/db/legacy-migration.d.ts +0 -41
  117. package/dist/esm/blueprint/db/legacy-migration.js +0 -122
@@ -1,60 +1,185 @@
1
1
  /**
2
- * `wp audit blueprint-lifecycle-sql` — SQL-backed rewrite of the existing
3
- * blueprint-lifecycle audit.
2
+ * `wp audit blueprint-lifecycle` — the single, deterministic blueprint-lifecycle
3
+ * audit.
4
4
  *
5
- * Uses the SQLite replica as the primary source when the DB file exists.
6
- * Falls back to the markdown-based audit when the DB has not been built yet.
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
- * SQL checks (when DB exists):
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` (e.g. stored in completed/ but status=in-progress).
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 { existsSync } from 'node:fs';
17
- import { migrateLegacyAgentDb } from '#db/legacy-migration.js';
18
- import { resolveBlueprintProjectionDbPath } from '#db/paths.js';
19
- // Legacy fallback path kept for the brief window where a migration may have
20
- // just-happened (Task 1.1 / F12 / R10 / E12). Worktree-scoped path is canonical.
21
- const LEGACY_DB_PATH = path.join('.agent', '.blueprints.db');
22
- export async function auditBlueprintLifecycleSql(cwd) {
23
- // F12/R10/E12: trigger one-shot migration before resolving the DB so a stray
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
- dbFile = resolveBlueprintProjectionDbPath(cwd);
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
- dbFile = path.join(cwd, LEGACY_DB_PATH);
42
+ return false;
37
43
  }
38
- if (!existsSync(dbFile)) {
39
- // DB not yet built — fall back to markdown-based audit
40
- const { auditBlueprintLifecycle } = await import('./repo-guardrails.js');
41
- return auditBlueprintLifecycle(cwd);
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
- const { Database } = await import('#db/sqlite.js');
44
- const db = new Database(dbFile, { readonly: true });
45
- const violations = [];
46
- let checked = 0;
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
- FROM blueprints
121
- WHERE status = 'completed'
122
- AND progress_pct IS NOT NULL
123
- AND progress_pct < 100`)
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: 'Blueprint lifecycle (SQL)',
384
+ title,
135
385
  checked,
136
- violations,
386
+ violations: [...violations, ...advisoryViolations],
137
387
  };
138
388
  }
139
389
  finally {
140
- db.close();
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 = resolve(execFileSync('git', ['rev-parse', '--show-toplevel'], {
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);