@gobing-ai/knowledge-kit 0.0.13 → 0.0.14
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 +1 -1
- package/dist/index.js +20 -0
- package/package.json +1 -1
- package/plugins/generations/content-gen/dist/index.js +20 -0
- package/plugins/generations/core-facts-gen/dist/index.js +20 -0
- package/plugins/generations/daily-article-gen/dist/index.js +29 -1
- package/plugins/generations/daily-article-gen/src/index.ts +13 -1
- package/plugins/generations/dailynews-gen/dist/index.js +20 -0
- package/plugins/generations/episode-plan-gen/dist/index.js +24 -0
- package/plugins/generations/episode-plan-gen/src/index.ts +11 -0
- package/plugins/generations/image-gen/dist/index.js +2296 -53
- package/plugins/generations/image-gen/src/providers/agnes.ts +110 -0
- package/plugins/generations/image-gen/src/providers/azure.ts +153 -0
- package/plugins/generations/image-gen/src/providers/codex-cli.ts +170 -0
- package/plugins/generations/image-gen/src/providers/dashscope.ts +485 -0
- package/plugins/generations/image-gen/src/providers/google.ts +268 -0
- package/plugins/generations/image-gen/src/providers/huggingface.ts +59 -0
- package/plugins/generations/image-gen/src/providers/jimeng.ts +259 -0
- package/plugins/generations/image-gen/src/providers/minimax.ts +171 -0
- package/plugins/generations/image-gen/src/providers/openai.ts +319 -0
- package/plugins/generations/image-gen/src/providers/openrouter.ts +257 -0
- package/plugins/generations/image-gen/src/providers/refs.ts +24 -0
- package/plugins/generations/image-gen/src/providers/replicate.ts +279 -0
- package/plugins/generations/image-gen/src/providers/seedream.ts +1 -1
- package/plugins/generations/image-gen/src/providers/types.ts +52 -51
- package/plugins/generations/image-gen/src/providers/zai.ts +237 -0
- package/plugins/generations/news-report-gen/dist/index.js +22193 -0
- package/plugins/generations/news-report-gen/package.json +17 -0
- package/plugins/generations/news-report-gen/plugin.json +7 -0
- package/plugins/generations/news-report-gen/src/index.ts +308 -0
- package/plugins/generations/news-report-gen/tsconfig.json +4 -0
- package/plugins/generations/omni-voice-gen/Makefile +14 -0
- package/plugins/generations/omni-voice-gen/README.md +112 -0
- package/plugins/generations/omni-voice-gen/bin/omni-voice-gen +2 -0
- package/plugins/generations/omni-voice-gen/dist/omni-voice-gen-prr8skpb. +2 -0
- package/plugins/generations/omni-voice-gen/dist/omni-voice-gen.js +6 -0
- package/plugins/generations/omni-voice-gen/plugin.json +6 -0
- package/plugins/generations/omni-voice-gen/profiles.json +12 -0
- package/plugins/generations/omni-voice-gen/pyproject.toml +25 -0
- package/plugins/generations/omni-voice-gen/scripts/coverage_gate.py +74 -0
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/__init__.py +1 -0
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/__main__.py +39 -0
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/audio.py +190 -0
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/backend.py +150 -0
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/contract.py +76 -0
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/mp3.py +60 -0
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/pipeline.py +289 -0
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/profiles.py +100 -0
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/qc.py +234 -0
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/voicescript.py +352 -0
- package/plugins/generations/omni-voice-gen/uv.lock +3510 -0
- package/plugins/generations/voice-gen/dist/index.js +30 -2
- package/plugins/generations/voice-gen/src/index.ts +16 -1
- package/plugins/generations/voice-gen/src/voicebox-client.ts +3 -1
- package/plugins/ingestions/aihot-ingest/dist/index.js +20 -0
- package/plugins/ingestions/horizon-ingest/dist/index.js +20 -0
- package/plugins/ingestions/last30days-ingest/dist/index.js +20 -0
- package/plugins/ingestions/web-search/dist/index.js +20 -0
- package/plugins/kk/commands/image-extract.md +40 -0
- package/plugins/kk/commands/image-generate.md +18 -1
- package/plugins/kk/plugin.json +1 -1
- package/plugins/kk/skills/image-authoring/SKILL.md +10 -3
- package/plugins/kk/skills/image-authoring/references/format-drafting.md +57 -0
- package/plugins/kk/skills/image-authoring/references/style-extraction.md +14 -9
- package/plugins/kk/workflows/kk-daily-ai-voice.yaml +130 -30
- package/plugins/publishings/emdash-pub/dist/index.js +20 -0
- package/plugins/publishings/podcast-pub/dist/index.js +57 -3
- package/plugins/publishings/podcast-pub/src/index.ts +18 -2
- package/plugins/publishings/podcast-pub/src/show-notes.ts +56 -9
- package/plugins/publishings/qiita-pub/dist/index.js +20 -0
- package/plugins/publishings/surfdash-pub/dist/index.js +96 -6
- package/plugins/publishings/surfdash-pub/src/index.ts +109 -9
- package/plugins/publishings/zenn-pub/dist/index.js +20 -0
- package/plugins/sp/scripts/batch-preflight.mjs +346 -0
- package/plugins/sp/scripts/batch-preflight.ts +459 -0
- package/plugins/sp/scripts/daily-summary/daily-summary.mjs +615 -0
- package/plugins/sp/scripts/daily-summary/daily-summary.ts +846 -0
- package/plugins/sp/scripts/daily-summary/logger.ts +28 -0
- package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.mjs +223 -0
- package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.ts +367 -0
- package/plugins/sp/scripts/dogfood-testing/validate-report.mjs +132 -0
- package/plugins/sp/scripts/dogfood-testing/validate-report.ts +169 -0
- package/plugins/sp/scripts/feature-dev-precheck.mjs +171 -0
- package/plugins/sp/scripts/feature-dev-precheck.ts +238 -0
- package/plugins/sp/scripts/feature-sync-bounded.mjs +285 -0
- package/plugins/sp/scripts/feature-sync-bounded.ts +478 -0
- package/plugins/sp/scripts/history-anatomy-cache.mjs +902 -0
- package/plugins/sp/scripts/history-anatomy-cache.ts +1028 -0
- package/plugins/sp/scripts/idea-handoff.mjs +22 -0
- package/plugins/sp/scripts/idea-handoff.ts +44 -0
- package/plugins/sp/scripts/inline-pipeline-parity-check.ts +185 -0
- package/plugins/sp/scripts/inline-run-setup.ts +198 -0
- package/plugins/sp/scripts/pr-reviewing.mjs +769 -0
- package/plugins/sp/scripts/pr-reviewing.ts +925 -0
- package/plugins/sp/scripts/quality-gate.mjs +179 -0
- package/plugins/sp/scripts/quality-gate.ts +217 -0
- package/plugins/sp/scripts/script-contract-check.ts +319 -0
- package/plugins/sp/scripts/stage-registry-adapter.ts +1533 -0
- package/plugins/sp/scripts/surface-drift-inventory.ts +929 -0
- package/plugins/sp/scripts/task-evidence-precheck.ts +181 -0
- package/plugins/sp/scripts/task-size-precheck.ts +175 -0
- package/plugins/sp/scripts/transition-shim-check.ts +238 -0
- package/plugins/sp/scripts/validate-commands.ts +689 -0
- package/plugins/sp/scripts/validate-flag-contracts.ts +878 -0
- package/plugins/sp/scripts/verify-answer-lint.ts +530 -0
- package/plugins/sp/scripts/workflow-step-profile.mjs +316 -0
- package/plugins/sp/scripts/workflow-step-profile.ts +456 -0
- package/plugins/sp/scripts/wrapup-steps.mjs +373 -0
- package/plugins/sp/scripts/wrapup-steps.ts +466 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* task-evidence-precheck — deterministic evidence-channel precheck (R2, task 0726).
|
|
4
|
+
*
|
|
5
|
+
* Parses the task content for an exact `evidence-channel:` declaration and proves the
|
|
6
|
+
* declared live-data channel exists in the local spur database before implementation
|
|
7
|
+
* begins. Currently exactly one channel is allowlisted:
|
|
8
|
+
*
|
|
9
|
+
* evidence-channel: history_tool_call.args_raw[pi]
|
|
10
|
+
*
|
|
11
|
+
* …satisfied only when the fixed query
|
|
12
|
+
*
|
|
13
|
+
* SELECT COUNT(*) FROM history_tool_call WHERE args_raw IS NOT NULL AND source = 'pi'
|
|
14
|
+
*
|
|
15
|
+
* returns a positive count on `<cwd>/.spur/spur.db` — i.e. a live non-dry-run pi import
|
|
16
|
+
* has already preserved tool-call `args_raw` (0722 R1). Unknown declarations, a missing
|
|
17
|
+
* database, a missing table, and a zero count all fail closed.
|
|
18
|
+
*
|
|
19
|
+
* A task without any `evidence-channel:` declaration passes without opening SQLite —
|
|
20
|
+
* the check only gates tasks that declare a live-data evidence channel.
|
|
21
|
+
*
|
|
22
|
+
* Always exits 0 (soft action). Both precheck→implement guards in task-pipeline.yaml
|
|
23
|
+
* read the status file; a missing or failing checker writes FAIL, so readiness fails
|
|
24
|
+
* closed.
|
|
25
|
+
*
|
|
26
|
+
* Ships with the plugin to arbitrary projects; node-builtin + bun:sqlite only —
|
|
27
|
+
* no workspace imports.
|
|
28
|
+
*
|
|
29
|
+
* Usage:
|
|
30
|
+
* bun plugins/sp/scripts/task-evidence-precheck.ts <wbs> [--spur-bin <path>]
|
|
31
|
+
*
|
|
32
|
+
* Env: SPUR_BIN
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import { Database } from 'bun:sqlite';
|
|
36
|
+
import { execFileSync } from 'node:child_process';
|
|
37
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
38
|
+
import { join } from 'node:path';
|
|
39
|
+
import { fileURLToPath } from 'node:url';
|
|
40
|
+
|
|
41
|
+
/** Exact task-content declaration that activates the live-evidence gate (0726 R2). */
|
|
42
|
+
const DECLARATION_PREFIX = 'evidence-channel:';
|
|
43
|
+
|
|
44
|
+
/** The only allowlisted live-data channel (0726 R2). */
|
|
45
|
+
const EVIDENCE_CHANNEL = 'history_tool_call.args_raw[pi]';
|
|
46
|
+
|
|
47
|
+
/** Declaration text as it must appear in the task body. */
|
|
48
|
+
const DECLARATION = `${DECLARATION_PREFIX} ${EVIDENCE_CHANNEL}`;
|
|
49
|
+
|
|
50
|
+
/** The only live-data query this precheck is allowed to run — fixed, never task-authored. */
|
|
51
|
+
const EVIDENCE_QUERY = "SELECT COUNT(*) AS n FROM history_tool_call WHERE args_raw IS NOT NULL AND source = 'pi'";
|
|
52
|
+
|
|
53
|
+
// ─── CLI (same spur-bin chain as task-size-precheck.ts) ─────────────────────
|
|
54
|
+
|
|
55
|
+
function usage(): never {
|
|
56
|
+
console.error('Usage: bun plugins/sp/scripts/task-evidence-precheck.ts <wbs> [--spur-bin <path>]');
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function defaultSpurBin(): string {
|
|
61
|
+
if (process.env.SPUR_BIN) return process.env.SPUR_BIN;
|
|
62
|
+
const local = fileURLToPath(new URL('../../../apps/cli/src/index.ts', import.meta.url));
|
|
63
|
+
if (existsSync(local)) return `bun ${local}`;
|
|
64
|
+
return 'spur';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function parseArgs(argv: string[]): { wbs: string; spurBin: string } {
|
|
68
|
+
let spurBin = defaultSpurBin();
|
|
69
|
+
let wbs = '';
|
|
70
|
+
let i = 0;
|
|
71
|
+
while (i < argv.length) {
|
|
72
|
+
const arg = argv[i];
|
|
73
|
+
if (arg === '--spur-bin') {
|
|
74
|
+
spurBin = argv[i + 1] ?? defaultSpurBin();
|
|
75
|
+
i += 2;
|
|
76
|
+
} else if (!arg.startsWith('--')) {
|
|
77
|
+
wbs = arg;
|
|
78
|
+
i++;
|
|
79
|
+
} else {
|
|
80
|
+
i++;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (!wbs) usage();
|
|
84
|
+
return { wbs, spurBin };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Split a multi-token `spurBin` (`<runtime> <mainModule>`) the same way
|
|
89
|
+
* `runSpurJson` does in feature-sync-bounded.ts — execFileSync's first arg is
|
|
90
|
+
* one executable path, not a shell command line.
|
|
91
|
+
*/
|
|
92
|
+
function runSpur(spurBin: string, args: string[]): string {
|
|
93
|
+
const [file = 'spur', ...lead] = spurBin.split(/\s+/).filter(Boolean);
|
|
94
|
+
return execFileSync(file, [...lead, ...args], {
|
|
95
|
+
encoding: 'utf-8',
|
|
96
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function writeStatus(wbs: string, status: 'PASS' | 'FAIL'): void {
|
|
101
|
+
const statusDir = join(process.cwd(), '.spur', 'run');
|
|
102
|
+
if (!existsSync(statusDir)) mkdirSync(statusDir, { recursive: true });
|
|
103
|
+
writeFileSync(join(statusDir, `${wbs}-precheck-evidence.status`), `${status}\n`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function fail(wbs: string, reasons: string[]): void {
|
|
107
|
+
writeStatus(wbs, 'FAIL');
|
|
108
|
+
console.error(`task-evidence-precheck: FAIL`);
|
|
109
|
+
for (const r of reasons) {
|
|
110
|
+
console.error(` ${r}`);
|
|
111
|
+
}
|
|
112
|
+
process.exit(0);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function main(): void {
|
|
116
|
+
const { wbs, spurBin } = parseArgs(process.argv.slice(2));
|
|
117
|
+
|
|
118
|
+
let taskContent: string;
|
|
119
|
+
try {
|
|
120
|
+
const result = runSpur(spurBin, ['task', 'show', wbs, '--json']);
|
|
121
|
+
const task = JSON.parse(result);
|
|
122
|
+
taskContent = task.content ?? task.body ?? '';
|
|
123
|
+
} catch {
|
|
124
|
+
fail(wbs, [`could not fetch task ${wbs} via ${spurBin} — evidence channel unverifiable`]);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Collect every declaration token. A repeated exact declaration still gates the
|
|
128
|
+
// single fixed query; any non-allowlisted token is an unknown declaration.
|
|
129
|
+
const declarations: string[] = [];
|
|
130
|
+
for (const match of taskContent.matchAll(/evidence-channel:\s*(\S+)/g)) {
|
|
131
|
+
declarations.push(match[1] ?? '');
|
|
132
|
+
}
|
|
133
|
+
const unknown = declarations.filter((d) => d !== EVIDENCE_CHANNEL);
|
|
134
|
+
if (unknown.length > 0) {
|
|
135
|
+
fail(wbs, [
|
|
136
|
+
`unknown evidence-channel declaration(s): ${unknown.join(', ')}`,
|
|
137
|
+
`allowlisted declaration: ${DECLARATION}`,
|
|
138
|
+
]);
|
|
139
|
+
}
|
|
140
|
+
if (declarations.length === 0) {
|
|
141
|
+
writeStatus(wbs, 'PASS');
|
|
142
|
+
console.error(`task-evidence-precheck: PASS — no evidence-channel declaration; live-data gate not active`);
|
|
143
|
+
process.exit(0);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const dbPath = join(process.cwd(), '.spur', 'spur.db');
|
|
147
|
+
if (!existsSync(dbPath)) {
|
|
148
|
+
fail(wbs, [`local spur database not found at ${dbPath} — run a real history import first`]);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
let count: number;
|
|
152
|
+
try {
|
|
153
|
+
const db = new Database(dbPath, { readonly: true });
|
|
154
|
+
try {
|
|
155
|
+
const row = db.query(EVIDENCE_QUERY).get() as { n: number } | undefined;
|
|
156
|
+
count = row?.n ?? 0;
|
|
157
|
+
} finally {
|
|
158
|
+
db.close();
|
|
159
|
+
}
|
|
160
|
+
} catch (e) {
|
|
161
|
+
fail(wbs, [
|
|
162
|
+
`evidence query failed on ${dbPath}: ${e instanceof Error ? e.message : String(e)}`,
|
|
163
|
+
'history_tool_call table missing or unreadable — run a real history import first',
|
|
164
|
+
]);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (!(count > 0)) {
|
|
168
|
+
fail(wbs, [
|
|
169
|
+
`0 live pi rows with args_raw (query: ${EVIDENCE_QUERY})`,
|
|
170
|
+
'run a non-dry-run pi history import with a safe importer before implementing',
|
|
171
|
+
]);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
writeStatus(wbs, 'PASS');
|
|
175
|
+
console.error(
|
|
176
|
+
`task-evidence-precheck: PASS — ${count} live pi history_tool_call row(s) with args_raw (declaration: ${DECLARATION})`,
|
|
177
|
+
);
|
|
178
|
+
process.exit(0);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
main();
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* task-size-precheck — pipeline size precheck guard (R2, task 0454; count-only
|
|
4
|
+
* since task 0723).
|
|
5
|
+
*
|
|
6
|
+
* Shells `spur task show <wbs> --json`, evaluates R-item count and Plan
|
|
7
|
+
* checklist count against limits, writes PASS/FAIL to status file. No executor
|
|
8
|
+
* or doctor involvement: executor liveness/routing/capabilities are attested
|
|
9
|
+
* fail-closed at the `agent.run` dispatch boundary, not predicted here.
|
|
10
|
+
*
|
|
11
|
+
* Always exits 0 (soft action). The precheck→implement guard in
|
|
12
|
+
* task-pipeline.yaml reads the status file; a missing or failing checker writes
|
|
13
|
+
* FAIL, so readiness fails closed.
|
|
14
|
+
*
|
|
15
|
+
* Ships with the plugin to arbitrary projects, so it stays node-builtin-only —
|
|
16
|
+
* no workspace imports.
|
|
17
|
+
*
|
|
18
|
+
* Usage:
|
|
19
|
+
* bun plugins/sp/scripts/task-size-precheck.ts <wbs> [--spur-bin <path>]
|
|
20
|
+
* [--max-reqs <n>] [--max-plan-items <n>]
|
|
21
|
+
*
|
|
22
|
+
* Env: SPUR_BIN, MAX_IMPLEMENT_REQS, MAX_IMPLEMENT_PLAN_ITEMS
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { execFileSync } from 'node:child_process';
|
|
26
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
27
|
+
import { join } from 'node:path';
|
|
28
|
+
import { fileURLToPath } from 'node:url';
|
|
29
|
+
|
|
30
|
+
// ─── Regex (sync with packages/app/src/services/task-size-precheck.ts) ───────
|
|
31
|
+
|
|
32
|
+
/** Matches `- [ ] **R1.**` or `- [x] R1.` etc. Requires period after digits. */
|
|
33
|
+
const R_ITEM_RE = /^\s*-\s*\[[ xX]\]\s*(\*\*)?R\d+\./m;
|
|
34
|
+
|
|
35
|
+
/** Matches checklist items under the Plan section. */
|
|
36
|
+
const CHECKLIST_ITEM_RE = /^\s*-\s*\[[ xX]\]/m;
|
|
37
|
+
|
|
38
|
+
// ─── CLI ─────────────────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
function usage(): never {
|
|
41
|
+
console.error(
|
|
42
|
+
'Usage: bun plugins/sp/scripts/task-size-precheck.ts <wbs> [--spur-bin <path>] [--max-reqs <n>] [--max-plan-items <n>]',
|
|
43
|
+
);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Resolve the spur CLI command in a monorepo-safe way:
|
|
49
|
+
* --spur-bin > SPUR_BIN > monorepo-local CLI entry > PATH `spur`.
|
|
50
|
+
* The plugin's own CI always passes an explicit --spur-bin; this fallback chain
|
|
51
|
+
* keeps ad-hoc invocations from silently hitting a stale PATH install.
|
|
52
|
+
*/
|
|
53
|
+
function defaultSpurBin(): string {
|
|
54
|
+
if (process.env.SPUR_BIN) return process.env.SPUR_BIN;
|
|
55
|
+
// scripts/ -> plugins/sp/ -> <repo>/apps/cli/src/index.ts (fileURLToPath — raw pathname breaks
|
|
56
|
+
// on %-encoded paths, e.g. spaces in the checkout directory)
|
|
57
|
+
const local = fileURLToPath(new URL('../../../apps/cli/src/index.ts', import.meta.url));
|
|
58
|
+
if (existsSync(local)) return `bun ${local}`;
|
|
59
|
+
return 'spur';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function parseArgs(argv: string[]): {
|
|
63
|
+
wbs: string;
|
|
64
|
+
spurBin: string;
|
|
65
|
+
maxReqs: number;
|
|
66
|
+
maxPlanItems: number;
|
|
67
|
+
} {
|
|
68
|
+
let spurBin = defaultSpurBin();
|
|
69
|
+
let wbs = '';
|
|
70
|
+
// Doubled deterministic ceiling (0723 operator decision): 10 R-items / 16
|
|
71
|
+
// Plan items — keep in sync with DEFAULT_TASK_SIZE_LIMITS in
|
|
72
|
+
// packages/app/src/services/task-size-precheck.ts (asserted by test).
|
|
73
|
+
let maxReqs = Number(process.env.MAX_IMPLEMENT_REQS) || 10;
|
|
74
|
+
let maxPlanItems = Number(process.env.MAX_IMPLEMENT_PLAN_ITEMS) || 16;
|
|
75
|
+
|
|
76
|
+
let i = 0;
|
|
77
|
+
while (i < argv.length) {
|
|
78
|
+
const arg = argv[i];
|
|
79
|
+
if (arg === '--spur-bin') {
|
|
80
|
+
spurBin = argv[i + 1] ?? defaultSpurBin();
|
|
81
|
+
i += 2;
|
|
82
|
+
} else if (arg === '--max-reqs') {
|
|
83
|
+
maxReqs = Number(argv[i + 1]) || 10;
|
|
84
|
+
i += 2;
|
|
85
|
+
} else if (arg === '--max-plan-items') {
|
|
86
|
+
maxPlanItems = Number(argv[i + 1]) || 16;
|
|
87
|
+
i += 2;
|
|
88
|
+
} else if (!arg.startsWith('--')) {
|
|
89
|
+
wbs = arg;
|
|
90
|
+
i++;
|
|
91
|
+
} else {
|
|
92
|
+
i++;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!wbs) usage();
|
|
97
|
+
return { wbs, spurBin, maxReqs, maxPlanItems };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Split a multi-token `spurBin` (`<runtime> <mainModule>`) the same way
|
|
102
|
+
* `runSpurJson` does in feature-sync-bounded.ts — execFileSync's first arg is
|
|
103
|
+
* one executable path, not a shell command line.
|
|
104
|
+
*/
|
|
105
|
+
function runSpur(spurBin: string, args: string[]): string {
|
|
106
|
+
const [file = 'spur', ...lead] = spurBin.split(/\s+/).filter(Boolean);
|
|
107
|
+
return execFileSync(file, [...lead, ...args], {
|
|
108
|
+
encoding: 'utf-8',
|
|
109
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function main(): void {
|
|
114
|
+
const { wbs, spurBin, maxReqs, maxPlanItems } = parseArgs(process.argv.slice(2));
|
|
115
|
+
|
|
116
|
+
// Fetch task content via spur
|
|
117
|
+
let taskContent: string;
|
|
118
|
+
try {
|
|
119
|
+
const result = runSpur(spurBin, ['task', 'show', wbs, '--json']);
|
|
120
|
+
const task = JSON.parse(result);
|
|
121
|
+
taskContent = task.content ?? task.body ?? '';
|
|
122
|
+
} catch {
|
|
123
|
+
// If spur fails, write FAIL and exit 0 — the status file carries the verdict
|
|
124
|
+
const statusDir = join(process.cwd(), '.spur', 'run');
|
|
125
|
+
if (!existsSync(statusDir)) mkdirSync(statusDir, { recursive: true });
|
|
126
|
+
writeFileSync(join(statusDir, `${wbs}-precheck-size.status`), 'FAIL\n');
|
|
127
|
+
console.error(`task-size-precheck: FAIL — could not fetch task ${wbs} via ${spurBin}`);
|
|
128
|
+
process.exit(0);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const reqCount = taskContent.match(new RegExp(R_ITEM_RE.source, 'gm'))?.length ?? 0;
|
|
132
|
+
|
|
133
|
+
const planMatch = taskContent.match(/^#{2,3}\s+Plan\s*$/m);
|
|
134
|
+
const planBody = planMatch
|
|
135
|
+
? (() => {
|
|
136
|
+
const rest = taskContent.slice((planMatch.index ?? 0) + planMatch[0].length);
|
|
137
|
+
const nextSection = rest.match(/^#{2,3}\s+/m);
|
|
138
|
+
return nextSection ? rest.slice(0, nextSection.index ?? 0) : rest;
|
|
139
|
+
})()
|
|
140
|
+
: '';
|
|
141
|
+
const planItemCount = planBody.match(new RegExp(CHECKLIST_ITEM_RE.source, 'gm'))?.length ?? 0;
|
|
142
|
+
|
|
143
|
+
const reasons: string[] = [];
|
|
144
|
+
if (reqCount > maxReqs) {
|
|
145
|
+
reasons.push(
|
|
146
|
+
`Task has ${reqCount} R-items (max ${maxReqs}). ` +
|
|
147
|
+
`Consider decomposing or raise maxImplementReqs via --vars.`,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
if (planItemCount > maxPlanItems) {
|
|
151
|
+
reasons.push(
|
|
152
|
+
`Task has ${planItemCount} Plan items (max ${maxPlanItems}). ` +
|
|
153
|
+
`Consider simplifying the plan or raise maxImplementPlanItems via --vars.`,
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const ok = reasons.length === 0;
|
|
158
|
+
const status = ok ? 'PASS' : 'FAIL';
|
|
159
|
+
|
|
160
|
+
const statusDir = join(process.cwd(), '.spur', 'run');
|
|
161
|
+
if (!existsSync(statusDir)) mkdirSync(statusDir, { recursive: true });
|
|
162
|
+
writeFileSync(join(statusDir, `${wbs}-precheck-size.status`), `${status}\n`);
|
|
163
|
+
|
|
164
|
+
const msg = `task-size-precheck: ${status} — ${reqCount} R-items, ${planItemCount} Plan items`;
|
|
165
|
+
console.error(msg);
|
|
166
|
+
if (!ok) {
|
|
167
|
+
for (const r of reasons) {
|
|
168
|
+
console.error(` ${r}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
process.exit(0);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
main();
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* transition-shim-check — two-sided gate over `config/transition-shims.json`
|
|
4
|
+
* (task 0541, feature B2).
|
|
5
|
+
*
|
|
6
|
+
* A transition shim is a compatibility path that must survive the agent-role
|
|
7
|
+
* transition. Every shim carries a source marker comment `@transition-shim(<id>)`
|
|
8
|
+
* so it is a grep target and a review signal, and a manifest entry in
|
|
9
|
+
* `config/transition-shims.json` records who owns it and when it can be removed.
|
|
10
|
+
*
|
|
11
|
+
* The gate is deliberately two-sided: a marker with no manifest entry
|
|
12
|
+
* FAILS, and a manifest entry whose marker no longer appears in source
|
|
13
|
+
* FAILS. Without the second half the manifest would rot into a permanent
|
|
14
|
+
* suppression list — the exact invisible-debt pattern this gate exists to
|
|
15
|
+
* end.
|
|
16
|
+
*
|
|
17
|
+
* Marker convention (docs/04_DESIGN.md §2.5):
|
|
18
|
+
* // @transition-shim(<id>) — <one line on what this keeps working>
|
|
19
|
+
* <id> matches ^[a-z0-9][a-z0-9-]*$ (lowercase kebab).
|
|
20
|
+
* Markers live in production source roots (apps, packages, plugins, config,
|
|
21
|
+
* scripts, tooling). Docs and test fixtures are not scanned — prose examples
|
|
22
|
+
* and gate-fixture marker text must not trip the gate.
|
|
23
|
+
*
|
|
24
|
+
* The manifest is the removal worklist: emptying it is the definition of the
|
|
25
|
+
* transition being complete (R4).
|
|
26
|
+
*
|
|
27
|
+
* Ships with the plugin to arbitrary projects, so it stays node-builtin-only —
|
|
28
|
+
* no workspace imports.
|
|
29
|
+
*
|
|
30
|
+
* Usage:
|
|
31
|
+
* bun plugins/sp/scripts/transition-shim-check.ts
|
|
32
|
+
* [--manifest <path>] default: config/transition-shims.json
|
|
33
|
+
* [--roots <a,b,c>] default: apps,packages,plugins,config,scripts,tooling
|
|
34
|
+
*
|
|
35
|
+
* Exit code: 0 when every manifest entry is present in source and every source
|
|
36
|
+
* marker is registered; 1 on any violation (unregistered marker, stale entry,
|
|
37
|
+
* or incomplete entry). Violations are printed to stderr; a summary to stdout.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
41
|
+
import { join, relative, resolve } from 'node:path';
|
|
42
|
+
|
|
43
|
+
const MARKER_RE = /@transition-shim\(\s*([a-z0-9][a-z0-9-]*)\s*\)/g;
|
|
44
|
+
const ID_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
45
|
+
|
|
46
|
+
/** Directories never scanned for markers (build output, vendored, VCS internals).
|
|
47
|
+
* `tests`/`test` are excluded too: a test fixture that mentions a marker id is
|
|
48
|
+
* data exercising the gate, not a compatibility path — shims live in production
|
|
49
|
+
* source, and only there do they need tracking. */
|
|
50
|
+
const SKIP_DIRS = new Set([
|
|
51
|
+
'node_modules',
|
|
52
|
+
'.git',
|
|
53
|
+
'dist',
|
|
54
|
+
'coverage',
|
|
55
|
+
'.spur',
|
|
56
|
+
'build',
|
|
57
|
+
'.next',
|
|
58
|
+
'.venv',
|
|
59
|
+
'__pycache__',
|
|
60
|
+
'vendors',
|
|
61
|
+
'_legacy_reference',
|
|
62
|
+
'artifacts',
|
|
63
|
+
'tests',
|
|
64
|
+
'test',
|
|
65
|
+
]);
|
|
66
|
+
|
|
67
|
+
const DEFAULT_ROOTS = ['apps', 'packages', 'plugins', 'config', 'scripts', 'tooling'];
|
|
68
|
+
|
|
69
|
+
/** One manifest record — every field is required (R1). */
|
|
70
|
+
interface ManifestEntry {
|
|
71
|
+
id: string;
|
|
72
|
+
wbs: string;
|
|
73
|
+
file: string;
|
|
74
|
+
keepsWorking: string;
|
|
75
|
+
removalCondition: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
interface Violation {
|
|
79
|
+
kind: 'unregistered' | 'stale' | 'incomplete';
|
|
80
|
+
id: string;
|
|
81
|
+
message: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function parseArgs(argv: string[]): { manifest: string; roots: string[]; cwd: string } {
|
|
85
|
+
let manifest = 'config/transition-shims.json';
|
|
86
|
+
let roots = DEFAULT_ROOTS;
|
|
87
|
+
for (let i = 0; i < argv.length; i++) {
|
|
88
|
+
if (argv[i] === '--manifest') manifest = argv[++i] ?? manifest;
|
|
89
|
+
else if (argv[i] === '--roots') roots = (argv[++i] ?? '').split(',').filter(Boolean);
|
|
90
|
+
}
|
|
91
|
+
return { manifest, roots, cwd: process.cwd() };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Walk a root and return marker id -> repo-relative files. */
|
|
95
|
+
function scanMarkers(cwd: string, roots: string[]): Map<string, Set<string>> {
|
|
96
|
+
const found = new Map<string, Set<string>>();
|
|
97
|
+
const walk = (dir: string): void => {
|
|
98
|
+
let entries: string[];
|
|
99
|
+
try {
|
|
100
|
+
entries = readdirSync(dir);
|
|
101
|
+
} catch {
|
|
102
|
+
return; // unreadable or missing root — skip
|
|
103
|
+
}
|
|
104
|
+
for (const name of entries) {
|
|
105
|
+
if (SKIP_DIRS.has(name)) continue;
|
|
106
|
+
const abs = join(dir, name);
|
|
107
|
+
let st: ReturnType<typeof statSync>;
|
|
108
|
+
try {
|
|
109
|
+
st = statSync(abs);
|
|
110
|
+
} catch {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (st.isDirectory()) {
|
|
114
|
+
walk(abs);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (!st.isFile()) continue;
|
|
118
|
+
let text: string;
|
|
119
|
+
try {
|
|
120
|
+
text = readFileSync(abs, 'utf8');
|
|
121
|
+
} catch {
|
|
122
|
+
continue; // binary or unreadable — markers are text only
|
|
123
|
+
}
|
|
124
|
+
for (const m of text.matchAll(MARKER_RE)) {
|
|
125
|
+
const id = m[1] as string;
|
|
126
|
+
const file = relative(cwd, abs);
|
|
127
|
+
const set = found.get(id) ?? new Set<string>();
|
|
128
|
+
set.add(file);
|
|
129
|
+
found.set(id, set);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
for (const root of roots) walk(resolve(cwd, root));
|
|
134
|
+
return found;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Load the manifest; a missing file degrades to no entries (fail-open). */
|
|
138
|
+
function loadManifest(path: string): { entries: ManifestEntry[]; raw: string | null } {
|
|
139
|
+
if (!existsSync(path)) return { entries: [], raw: null };
|
|
140
|
+
const raw = readFileSync(path, 'utf8');
|
|
141
|
+
let parsed: unknown;
|
|
142
|
+
try {
|
|
143
|
+
parsed = JSON.parse(raw);
|
|
144
|
+
} catch (error) {
|
|
145
|
+
return { entries: [], raw: `malformed JSON at ${path}: ${String(error)}` };
|
|
146
|
+
}
|
|
147
|
+
const entries = (parsed as { entries?: unknown[] }).entries;
|
|
148
|
+
if (!Array.isArray(entries)) return { entries: [], raw: `${path}: missing "entries" array` };
|
|
149
|
+
return { entries: entries as ManifestEntry[], raw: null };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function validateEntry(e: ManifestEntry, violations: Violation[]): void {
|
|
153
|
+
const field = (key: keyof ManifestEntry, value: unknown, why: string): void => {
|
|
154
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
155
|
+
violations.push({
|
|
156
|
+
kind: 'incomplete',
|
|
157
|
+
id: typeof e.id === 'string' ? e.id : '<missing>',
|
|
158
|
+
message: `manifest entry ${JSON.stringify(e.id ?? null)} is missing required field "${key}"${why}`,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
field('id', e.id, '');
|
|
163
|
+
field('wbs', e.wbs, ` in entry ${JSON.stringify(e.id ?? null)}`);
|
|
164
|
+
field('file', e.file, ` in entry ${JSON.stringify(e.id ?? null)}`);
|
|
165
|
+
field('keepsWorking', e.keepsWorking, ` in entry ${JSON.stringify(e.id ?? null)}`);
|
|
166
|
+
field('removalCondition', e.removalCondition, ` in entry ${JSON.stringify(e.id ?? null)}`);
|
|
167
|
+
if (typeof e.id === 'string' && e.id !== '' && !ID_RE.test(e.id)) {
|
|
168
|
+
violations.push({
|
|
169
|
+
kind: 'incomplete',
|
|
170
|
+
id: e.id,
|
|
171
|
+
message: `manifest entry id ${JSON.stringify(e.id)} is not lowercase-kebab (^[a-z0-9][a-z0-9-]*$)`,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function run(): number {
|
|
177
|
+
const { manifest, roots, cwd } = parseArgs(process.argv.slice(2));
|
|
178
|
+
const manifestPath = resolve(cwd, manifest);
|
|
179
|
+
const { entries, raw } = loadManifest(manifestPath);
|
|
180
|
+
const violations: Violation[] = [];
|
|
181
|
+
const incomplete = new Set<string>();
|
|
182
|
+
|
|
183
|
+
if (raw !== null) {
|
|
184
|
+
violations.push({ kind: 'incomplete', id: '<manifest>', message: raw });
|
|
185
|
+
// A manifest that cannot be parsed has no valid entries to compare against.
|
|
186
|
+
for (const v of violations) console.error(`transition-shim-check: FAIL - ${v.message}`);
|
|
187
|
+
console.error('transition-shim-check: FAIL - manifest unreadable; refusing to pass');
|
|
188
|
+
return 1;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
for (const e of entries) {
|
|
192
|
+
const before = violations.length;
|
|
193
|
+
validateEntry(e, violations);
|
|
194
|
+
if (violations.length > before) incomplete.add(String(e.id));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const markers = scanMarkers(cwd, roots);
|
|
198
|
+
const observedIds = new Set(markers.keys());
|
|
199
|
+
const manifestIds = new Set(entries.filter((e) => !incomplete.has(String(e.id))).map((e) => e.id));
|
|
200
|
+
|
|
201
|
+
for (const [id, files] of markers) {
|
|
202
|
+
if (manifestIds.has(id)) continue;
|
|
203
|
+
const file = [...files].sort().join(', ');
|
|
204
|
+
violations.push({
|
|
205
|
+
kind: 'unregistered',
|
|
206
|
+
id,
|
|
207
|
+
message: `new unregistered shim @transition-shim(${id}) in ${file} — add an entry to ${manifest} or remove the marker`,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
for (const e of entries) {
|
|
211
|
+
if (incomplete.has(e.id)) continue;
|
|
212
|
+
if (observedIds.has(e.id)) continue;
|
|
213
|
+
violations.push({
|
|
214
|
+
kind: 'stale',
|
|
215
|
+
id: e.id,
|
|
216
|
+
message: `stale manifest entry ${e.id} (wbs ${e.wbs}) — @transition-shim(${e.id}) no longer appears in source; remove the entry, its removal condition was: ${e.removalCondition}`,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const unregistered = violations.filter((v) => v.kind === 'unregistered');
|
|
221
|
+
const stale = violations.filter((v) => v.kind === 'stale');
|
|
222
|
+
const incompleteCount = violations.filter((v) => v.kind === 'incomplete').length;
|
|
223
|
+
const ok = violations.length === 0;
|
|
224
|
+
|
|
225
|
+
const noun = entries.length === 1 ? 'entry' : 'entries';
|
|
226
|
+
console.log(
|
|
227
|
+
`transition-shim-check: ${markers.size} marker(s) observed, ${entries.length} manifest ${noun} baselined, ` +
|
|
228
|
+
`${unregistered.length} new, ${stale.length} stale, ${incompleteCount} incomplete — ${ok ? 'PASS' : 'FAIL'}`,
|
|
229
|
+
);
|
|
230
|
+
if (ok) return 0;
|
|
231
|
+
|
|
232
|
+
for (const v of violations) {
|
|
233
|
+
console.error(`transition-shim-check: ${v.kind === 'incomplete' ? 'FAIL' : `FAIL (${v.kind})`} - ${v.message}`);
|
|
234
|
+
}
|
|
235
|
+
return 1;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
process.exit(run());
|