@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,132 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
|
|
4
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
5
|
+
|
|
6
|
+
// plugins/sp/scripts/dogfood-testing/validate-report.ts
|
|
7
|
+
import { readFileSync } from 'node:fs';
|
|
8
|
+
|
|
9
|
+
var REQUIRED_SECTIONS = [1, 2, 3, 4, 5, 6];
|
|
10
|
+
var CANONICAL_PROTOCOL = 'sp:dogfood-testing@1.2';
|
|
11
|
+
function countSectionHeadings(markdown, section) {
|
|
12
|
+
const re = new RegExp(`^### ${section}\\.`, 'gm');
|
|
13
|
+
return (markdown.match(re) ?? []).length;
|
|
14
|
+
}
|
|
15
|
+
function countLedgerDataRows(markdown) {
|
|
16
|
+
const heading = markdown.match(/^### 3\. Monitor Ledger\s*$/m);
|
|
17
|
+
if (!heading || heading.index === undefined) return null;
|
|
18
|
+
const after = markdown.slice(heading.index + heading[0].length);
|
|
19
|
+
const nextHeading = after.search(/^### /m);
|
|
20
|
+
const body = nextHeading === -1 ? after : after.slice(0, nextHeading);
|
|
21
|
+
const rows = body
|
|
22
|
+
.split(`
|
|
23
|
+
`)
|
|
24
|
+
.filter((line) => line.trim().startsWith('|'))
|
|
25
|
+
.filter((line) => !/^\|[\s:|-]+\|?\s*$/.test(line.trim()))
|
|
26
|
+
.filter((line) => !/^\|\s*`?drift:/.test(line.trim()));
|
|
27
|
+
return Math.max(rows.length - 1, 0);
|
|
28
|
+
}
|
|
29
|
+
function declaredExecutedSteps(markdown) {
|
|
30
|
+
const match = markdown.match(/\*\*Steps:\*\*\s*\d+\s+derived,\s*(\d+)\s+executed/);
|
|
31
|
+
return match ? Number.parseInt(match[1], 10) : null;
|
|
32
|
+
}
|
|
33
|
+
function validateReport(markdown) {
|
|
34
|
+
const errors = [];
|
|
35
|
+
if (!markdown.includes('── Dogfood Summary ──')) errors.push('missing_footer');
|
|
36
|
+
if (!markdown.includes('[Live:')) errors.push('missing_live_path');
|
|
37
|
+
if (!markdown.includes('[Report:')) errors.push('missing_report_path');
|
|
38
|
+
for (const section of REQUIRED_SECTIONS) {
|
|
39
|
+
const count = countSectionHeadings(markdown, section);
|
|
40
|
+
if (count === 0) errors.push(`missing_section:${section}`);
|
|
41
|
+
if (count > 1) errors.push(`duplicate_section:${section}`);
|
|
42
|
+
}
|
|
43
|
+
if (!markdown.includes('#### Fixed') || !markdown.includes('#### Unresolved')) {
|
|
44
|
+
errors.push('missing_issues_subheads');
|
|
45
|
+
}
|
|
46
|
+
const protocolMatch = markdown.match(/^protocol:\s*(\S+)\s*$/m);
|
|
47
|
+
if (!protocolMatch || protocolMatch[1] !== CANONICAL_PROTOCOL) {
|
|
48
|
+
errors.push('protocol_string');
|
|
49
|
+
}
|
|
50
|
+
const executed = declaredExecutedSteps(markdown);
|
|
51
|
+
if (executed === null) {
|
|
52
|
+
errors.push('missing_steps_declared');
|
|
53
|
+
} else {
|
|
54
|
+
const rows = countLedgerDataRows(markdown);
|
|
55
|
+
if (rows === null || rows !== executed) {
|
|
56
|
+
errors.push('ledger_cardinality');
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return { ok: errors.length === 0, errors };
|
|
60
|
+
}
|
|
61
|
+
function parseValidateCliArgs(argv) {
|
|
62
|
+
let file = null;
|
|
63
|
+
let json = false;
|
|
64
|
+
let help = false;
|
|
65
|
+
for (let i = 0; i < argv.length; i++) {
|
|
66
|
+
const a = argv[i];
|
|
67
|
+
if (a === '--help' || a === '-h') help = true;
|
|
68
|
+
else if (a === '--json') json = true;
|
|
69
|
+
else if (a === '--file') file = argv[++i] ?? null;
|
|
70
|
+
else if (!a.startsWith('-') && file === null) file = a;
|
|
71
|
+
}
|
|
72
|
+
return { file, json, help };
|
|
73
|
+
}
|
|
74
|
+
var VALIDATE_CLI_USAGE = `Usage:
|
|
75
|
+
bun plugins/sp/scripts/dogfood-testing/validate-report.ts --file <report.md> [--json]
|
|
76
|
+
|
|
77
|
+
Exit codes:
|
|
78
|
+
0 report validates clean (complete-report shape)
|
|
79
|
+
2 validation failed (errors on stdout / --json)
|
|
80
|
+
1 usage error
|
|
81
|
+
|
|
82
|
+
Phase 4 finalize MUST run this before status: complete (task 0278 R6).
|
|
83
|
+
On exit 2: set status: aborted and list error codes under #### Unresolved.`;
|
|
84
|
+
function runValidateCli(argv, readFile) {
|
|
85
|
+
const { file, json, help } = parseValidateCliArgs(argv);
|
|
86
|
+
if (help) return { exitCode: 0, stdout: '', stderr: VALIDATE_CLI_USAGE };
|
|
87
|
+
if (file === null || file.length === 0) {
|
|
88
|
+
return { exitCode: 1, stdout: '', stderr: VALIDATE_CLI_USAGE };
|
|
89
|
+
}
|
|
90
|
+
let markdown;
|
|
91
|
+
try {
|
|
92
|
+
markdown = readFile(file);
|
|
93
|
+
} catch (err) {
|
|
94
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
95
|
+
return { exitCode: 1, stdout: '', stderr: `Failed to read ${file}: ${msg}` };
|
|
96
|
+
}
|
|
97
|
+
const result = validateReport(markdown);
|
|
98
|
+
if (json) {
|
|
99
|
+
return {
|
|
100
|
+
exitCode: result.ok ? 0 : 2,
|
|
101
|
+
stdout: `${JSON.stringify(result, null, 2)}
|
|
102
|
+
`,
|
|
103
|
+
stderr: '',
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
if (result.ok) {
|
|
107
|
+
return {
|
|
108
|
+
exitCode: 0,
|
|
109
|
+
stdout: `ok
|
|
110
|
+
`,
|
|
111
|
+
stderr: '',
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
exitCode: 2,
|
|
116
|
+
stdout: `${result.errors.join(`
|
|
117
|
+
`)}
|
|
118
|
+
`,
|
|
119
|
+
stderr: '',
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
function mainCli(argv = process.argv.slice(2)) {
|
|
123
|
+
const { exitCode, stdout, stderr } = runValidateCli(argv, (p) => readFileSync(p, 'utf8'));
|
|
124
|
+
if (stdout) process.stdout.write(stdout);
|
|
125
|
+
if (stderr)
|
|
126
|
+
process.stderr.write(`${stderr}
|
|
127
|
+
`);
|
|
128
|
+
return exitCode;
|
|
129
|
+
}
|
|
130
|
+
process.exit(mainCli());
|
|
131
|
+
|
|
132
|
+
export { mainCli, parseValidateCliArgs, runValidateCli, VALIDATE_CLI_USAGE, validateReport };
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* validate-report — @1.2 dogfood report contract checker (task 0276, W6).
|
|
3
|
+
*
|
|
4
|
+
* Pure function over a report markdown string → { ok, errors[] } with stable error
|
|
5
|
+
* codes, callable from tests and by agents finalizing a run. It validates the
|
|
6
|
+
* *complete-report* shape: the six unique section headings, Issues subheads, the
|
|
7
|
+
* mandatory summary footer with both delivery paths, frontmatter protocol string,
|
|
8
|
+
* and ledger↔declared-steps cardinality. Aborted/partial reports are out of scope —
|
|
9
|
+
* they legitimately lack the footer and Steps line.
|
|
10
|
+
*
|
|
11
|
+
* CLI (task 0278 R6): `bun …/validate-report.ts --file <report.md> [--json]`
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { readFileSync } from 'node:fs';
|
|
15
|
+
|
|
16
|
+
export interface ReportValidation {
|
|
17
|
+
ok: boolean;
|
|
18
|
+
errors: string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const REQUIRED_SECTIONS = [1, 2, 3, 4, 5, 6] as const;
|
|
22
|
+
const CANONICAL_PROTOCOL = 'sp:dogfood-testing@1.2';
|
|
23
|
+
|
|
24
|
+
function countSectionHeadings(markdown: string, section: number): number {
|
|
25
|
+
const re = new RegExp(`^### ${section}\\.`, 'gm');
|
|
26
|
+
return (markdown.match(re) ?? []).length;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function countLedgerDataRows(markdown: string): number | null {
|
|
30
|
+
const heading = markdown.match(/^### 3\. Monitor Ledger\s*$/m);
|
|
31
|
+
if (!heading || heading.index === undefined) return null;
|
|
32
|
+
const after = markdown.slice(heading.index + heading[0].length);
|
|
33
|
+
const nextHeading = after.search(/^### /m);
|
|
34
|
+
const body = nextHeading === -1 ? after : after.slice(0, nextHeading);
|
|
35
|
+
const rows = body
|
|
36
|
+
.split('\n')
|
|
37
|
+
.filter((line) => line.trim().startsWith('|'))
|
|
38
|
+
.filter((line) => !/^\|[\s:|-]+\|?\s*$/.test(line.trim()))
|
|
39
|
+
// drift:external rows are documentary (task 0296) — included in the table but
|
|
40
|
+
// subtracted from the executed-step count, per the @1.2 cardinality contract;
|
|
41
|
+
// the prescribed code-span form (`drift:external`) matches too (task 0701 R5b).
|
|
42
|
+
.filter((line) => !/^\|\s*`?drift:/.test(line.trim()));
|
|
43
|
+
// Minus the header row; what remains are data rows.
|
|
44
|
+
return Math.max(rows.length - 1, 0);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function declaredExecutedSteps(markdown: string): number | null {
|
|
48
|
+
const match = markdown.match(/\*\*Steps:\*\*\s*\d+\s+derived,\s*(\d+)\s+executed/);
|
|
49
|
+
return match ? Number.parseInt(match[1], 10) : null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function validateReport(markdown: string): ReportValidation {
|
|
53
|
+
const errors: string[] = [];
|
|
54
|
+
|
|
55
|
+
// Footer (W2/D1) — the mirrored footer block at the report end.
|
|
56
|
+
if (!markdown.includes('── Dogfood Summary ──')) errors.push('missing_footer');
|
|
57
|
+
if (!markdown.includes('[Live:')) errors.push('missing_live_path');
|
|
58
|
+
if (!markdown.includes('[Report:')) errors.push('missing_report_path');
|
|
59
|
+
|
|
60
|
+
// Six unique section headings (W2/D3).
|
|
61
|
+
for (const section of REQUIRED_SECTIONS) {
|
|
62
|
+
const count = countSectionHeadings(markdown, section);
|
|
63
|
+
if (count === 0) errors.push(`missing_section:${section}`);
|
|
64
|
+
if (count > 1) errors.push(`duplicate_section:${section}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Issues subheads (W2/D3).
|
|
68
|
+
if (!markdown.includes('#### Fixed') || !markdown.includes('#### Unresolved')) {
|
|
69
|
+
errors.push('missing_issues_subheads');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Protocol string (W1/D5) — colon form, exact version.
|
|
73
|
+
const protocolMatch = markdown.match(/^protocol:\s*(\S+)\s*$/m);
|
|
74
|
+
if (!protocolMatch || protocolMatch[1] !== CANONICAL_PROTOCOL) {
|
|
75
|
+
errors.push('protocol_string');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Ledger cardinality (W3/D4) — data rows == declared executed steps.
|
|
79
|
+
const executed = declaredExecutedSteps(markdown);
|
|
80
|
+
if (executed === null) {
|
|
81
|
+
errors.push('missing_steps_declared');
|
|
82
|
+
} else {
|
|
83
|
+
const rows = countLedgerDataRows(markdown);
|
|
84
|
+
if (rows === null || rows !== executed) {
|
|
85
|
+
errors.push('ledger_cardinality');
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return { ok: errors.length === 0, errors };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ── CLI entry (Phase 4 self-validate — task 0278 R6) ─────────────────────────
|
|
93
|
+
|
|
94
|
+
export interface ValidateCliArgs {
|
|
95
|
+
file: string | null;
|
|
96
|
+
json: boolean;
|
|
97
|
+
help: boolean;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function parseValidateCliArgs(argv: string[]): ValidateCliArgs {
|
|
101
|
+
let file: string | null = null;
|
|
102
|
+
let json = false;
|
|
103
|
+
let help = false;
|
|
104
|
+
for (let i = 0; i < argv.length; i++) {
|
|
105
|
+
const a = argv[i];
|
|
106
|
+
if (a === '--help' || a === '-h') help = true;
|
|
107
|
+
else if (a === '--json') json = true;
|
|
108
|
+
else if (a === '--file') file = argv[++i] ?? null;
|
|
109
|
+
else if (!a.startsWith('-') && file === null) file = a;
|
|
110
|
+
}
|
|
111
|
+
return { file, json, help };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export const VALIDATE_CLI_USAGE = `Usage:
|
|
115
|
+
bun plugins/sp/scripts/dogfood-testing/validate-report.ts --file <report.md> [--json]
|
|
116
|
+
|
|
117
|
+
Exit codes:
|
|
118
|
+
0 report validates clean (complete-report shape)
|
|
119
|
+
2 validation failed (errors on stdout / --json)
|
|
120
|
+
1 usage error
|
|
121
|
+
|
|
122
|
+
Phase 4 finalize MUST run this before status: complete (task 0278 R6).
|
|
123
|
+
On exit 2: set status: aborted and list error codes under #### Unresolved.`;
|
|
124
|
+
|
|
125
|
+
export function runValidateCli(
|
|
126
|
+
argv: string[],
|
|
127
|
+
readFile: (path: string) => string,
|
|
128
|
+
): { exitCode: number; stdout: string; stderr: string } {
|
|
129
|
+
const { file, json, help } = parseValidateCliArgs(argv);
|
|
130
|
+
if (help) return { exitCode: 0, stdout: '', stderr: VALIDATE_CLI_USAGE };
|
|
131
|
+
if (file === null || file.length === 0) {
|
|
132
|
+
return { exitCode: 1, stdout: '', stderr: VALIDATE_CLI_USAGE };
|
|
133
|
+
}
|
|
134
|
+
let markdown: string;
|
|
135
|
+
try {
|
|
136
|
+
markdown = readFile(file);
|
|
137
|
+
} catch (err) {
|
|
138
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
139
|
+
return { exitCode: 1, stdout: '', stderr: `Failed to read ${file}: ${msg}` };
|
|
140
|
+
}
|
|
141
|
+
const result = validateReport(markdown);
|
|
142
|
+
if (json) {
|
|
143
|
+
return {
|
|
144
|
+
exitCode: result.ok ? 0 : 2,
|
|
145
|
+
stdout: `${JSON.stringify(result, null, 2)}\n`,
|
|
146
|
+
stderr: '',
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
if (result.ok) {
|
|
150
|
+
return { exitCode: 0, stdout: 'ok\n', stderr: '' };
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
exitCode: 2,
|
|
154
|
+
stdout: `${result.errors.join('\n')}\n`,
|
|
155
|
+
stderr: '',
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** CLI entry for Phase 4 self-validate (import.meta.main). */
|
|
160
|
+
export function mainCli(argv: string[] = process.argv.slice(2)): number {
|
|
161
|
+
const { exitCode, stdout, stderr } = runValidateCli(argv, (p) => readFileSync(p, 'utf8'));
|
|
162
|
+
if (stdout) process.stdout.write(stdout);
|
|
163
|
+
if (stderr) process.stderr.write(`${stderr}\n`);
|
|
164
|
+
return exitCode;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (import.meta.main) {
|
|
168
|
+
process.exit(mainCli());
|
|
169
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @bun
|
|
3
|
+
|
|
4
|
+
// plugins/sp/scripts/feature-dev-precheck.ts
|
|
5
|
+
import { spawnSync } from 'node:child_process';
|
|
6
|
+
import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
|
|
9
|
+
var BLOCKING_STATUSES = ['backlog', 'wip', 'testing', 'blocked'];
|
|
10
|
+
var KNOWN_STATUSES = ['todo', 'done', 'cancelled', ...BLOCKING_STATUSES];
|
|
11
|
+
function identityRc(featureId, runId) {
|
|
12
|
+
return featureId && runId ? 0 : 1;
|
|
13
|
+
}
|
|
14
|
+
function featureIdentityMatches(raw, featureId) {
|
|
15
|
+
let parsed;
|
|
16
|
+
try {
|
|
17
|
+
parsed = JSON.parse(raw);
|
|
18
|
+
} catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
return typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed) && parsed.id === featureId;
|
|
22
|
+
}
|
|
23
|
+
function rosterIsNonEmptyArray(raw) {
|
|
24
|
+
let parsed;
|
|
25
|
+
try {
|
|
26
|
+
parsed = JSON.parse(raw);
|
|
27
|
+
} catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
return Array.isArray(parsed) && parsed.length > 0;
|
|
31
|
+
}
|
|
32
|
+
function rosterContractHolds(rows) {
|
|
33
|
+
const seen = new Set();
|
|
34
|
+
for (const row of rows) {
|
|
35
|
+
if (row === null || typeof row !== 'object') return false;
|
|
36
|
+
if (typeof row.wbs !== 'string' || row.wbs.length === 0) return false;
|
|
37
|
+
if (seen.has(row.wbs)) return false;
|
|
38
|
+
seen.add(row.wbs);
|
|
39
|
+
if (typeof row.status !== 'string' || !KNOWN_STATUSES.includes(row.status)) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
function hasBlockingStatus(rows) {
|
|
46
|
+
return rows.some((row) => typeof row.status === 'string' && BLOCKING_STATUSES.includes(row.status));
|
|
47
|
+
}
|
|
48
|
+
function freezeTodoList(rows) {
|
|
49
|
+
return rows
|
|
50
|
+
.filter((row) => row.status === 'todo' && typeof row.wbs === 'string')
|
|
51
|
+
.map((row) => row.wbs)
|
|
52
|
+
.sort()
|
|
53
|
+
.join(',');
|
|
54
|
+
}
|
|
55
|
+
function spurCommand(spurBin) {
|
|
56
|
+
const parts = (spurBin ?? 'spur')
|
|
57
|
+
.trim()
|
|
58
|
+
.split(/\s+/)
|
|
59
|
+
.filter((p) => p.length > 0);
|
|
60
|
+
return { cmd: parts[0] ?? 'spur', prefix: parts.slice(1) };
|
|
61
|
+
}
|
|
62
|
+
function readFileSyncRaw(path) {
|
|
63
|
+
try {
|
|
64
|
+
return readFileSync(path, 'utf8');
|
|
65
|
+
} catch {
|
|
66
|
+
return '';
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function runFeatureDevPrecheck(env, options = {}) {
|
|
70
|
+
const cwd = options.cwd;
|
|
71
|
+
const runDir = join('.spur', 'run');
|
|
72
|
+
mkdirSync(cwd ? join(cwd, runDir) : runDir, { recursive: true });
|
|
73
|
+
const abs = (p) => (cwd ? join(cwd, p) : p);
|
|
74
|
+
const featureFile = join(runDir, `${env.__runId ?? ''}-feature-dev-feature.json`);
|
|
75
|
+
const rosterFile = join(runDir, `${env.__runId ?? ''}-feature-dev-roster.json`);
|
|
76
|
+
const tasksFile = join(runDir, `${env.__runId ?? ''}-feature-dev-tasks.txt`);
|
|
77
|
+
const statusFile = join(runDir, `${env.__runId ?? ''}-feature-dev-precheck.status`);
|
|
78
|
+
const featureId = env.featureId ?? '';
|
|
79
|
+
rmSync(abs(statusFile), { force: true });
|
|
80
|
+
const { cmd, prefix } = spurCommand(env.spurBin);
|
|
81
|
+
const read = (args, target) => {
|
|
82
|
+
const result = spawnSync(cmd, [...prefix, ...args], {
|
|
83
|
+
cwd,
|
|
84
|
+
encoding: 'utf8',
|
|
85
|
+
...(options.env ? { env: { ...process.env, ...options.env } } : {}),
|
|
86
|
+
});
|
|
87
|
+
if (result.error !== undefined) {
|
|
88
|
+
writeFileSync(
|
|
89
|
+
abs(target),
|
|
90
|
+
`spawn failed: ${result.error.message}
|
|
91
|
+
`,
|
|
92
|
+
);
|
|
93
|
+
return 127;
|
|
94
|
+
}
|
|
95
|
+
writeFileSync(abs(target), `${result.stdout ?? ''}${result.stderr ?? ''}`);
|
|
96
|
+
return result.status ?? 1;
|
|
97
|
+
};
|
|
98
|
+
const idRc = identityRc(featureId, env.__runId);
|
|
99
|
+
const showRc = idRc === 0 ? read(['feature', 'show', featureId, '--json'], featureFile) : 1;
|
|
100
|
+
const listRc =
|
|
101
|
+
idRc === 0 && showRc === 0 && featureIdentityMatches(readFileSyncRaw(abs(featureFile)), featureId)
|
|
102
|
+
? read(['task', 'list', '--feature', featureId, '--json'], rosterFile)
|
|
103
|
+
: 1;
|
|
104
|
+
const fail = (message) => {
|
|
105
|
+
process.stderr.write(`${message}
|
|
106
|
+
`);
|
|
107
|
+
writeFileSync(
|
|
108
|
+
abs(statusFile),
|
|
109
|
+
`FAIL
|
|
110
|
+
`,
|
|
111
|
+
);
|
|
112
|
+
return { status: 'FAIL', featureFile, rosterFile, tasksFile, statusFile, tasks: '' };
|
|
113
|
+
};
|
|
114
|
+
if (idRc !== 0 || showRc !== 0 || listRc !== 0) {
|
|
115
|
+
return fail(
|
|
116
|
+
`feature-dev precheck: missing featureId/runId, unknown feature '${featureId}', or unreadable roster (rc ${idRc}/${showRc}/${listRc}) \u2014 supply an existing planned feature via /sp:dev-plan or /sp:dev-idea; nothing was auto-created or re-planned`,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
const rosterRaw = readFileSyncRaw(abs(rosterFile));
|
|
120
|
+
if (!rosterIsNonEmptyArray(rosterRaw)) {
|
|
121
|
+
return fail(
|
|
122
|
+
`feature-dev precheck: roster at ${rosterFile} is malformed, not an array, or empty \u2014 plan the feature first via /sp:dev-plan; refusing to replan or run an empty batch`,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
const rows = JSON.parse(rosterRaw);
|
|
126
|
+
if (!rosterContractHolds(rows)) {
|
|
127
|
+
return fail(
|
|
128
|
+
`feature-dev precheck: roster has empty/duplicate/mismatched WBS identities or unknown statuses at ${rosterFile} \u2014 repair the task corpus; refusing to batch a broken roster`,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
if (hasBlockingStatus(rows)) {
|
|
132
|
+
return fail(
|
|
133
|
+
'feature-dev precheck: linked task(s) are backlog/wip/testing/blocked \u2014 refine or resume them through their own task pipelines before batching; refusing to launch overlapping work',
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
const tasks = freezeTodoList(rows);
|
|
137
|
+
writeFileSync(abs(tasksFile), tasks);
|
|
138
|
+
writeFileSync(
|
|
139
|
+
abs(statusFile),
|
|
140
|
+
`PASS
|
|
141
|
+
`,
|
|
142
|
+
);
|
|
143
|
+
return { status: 'PASS', featureFile, rosterFile, tasksFile, statusFile, tasks };
|
|
144
|
+
}
|
|
145
|
+
var FEATURE_DEV_PRECHECK_USAGE =
|
|
146
|
+
'usage: feature-dev-precheck.ts (env: featureId, __runId, optional spurBin) \u2014 no subcommands';
|
|
147
|
+
function main(argv, env = process.env) {
|
|
148
|
+
if (argv.length > 0) {
|
|
149
|
+
process.stderr.write(`${FEATURE_DEV_PRECHECK_USAGE}
|
|
150
|
+
`);
|
|
151
|
+
return 2;
|
|
152
|
+
}
|
|
153
|
+
runFeatureDevPrecheck(env);
|
|
154
|
+
return 0;
|
|
155
|
+
}
|
|
156
|
+
process.exit(main(process.argv.slice(2)));
|
|
157
|
+
|
|
158
|
+
export {
|
|
159
|
+
BLOCKING_STATUSES,
|
|
160
|
+
FEATURE_DEV_PRECHECK_USAGE,
|
|
161
|
+
featureIdentityMatches,
|
|
162
|
+
freezeTodoList,
|
|
163
|
+
hasBlockingStatus,
|
|
164
|
+
identityRc,
|
|
165
|
+
KNOWN_STATUSES,
|
|
166
|
+
main,
|
|
167
|
+
rosterContractHolds,
|
|
168
|
+
rosterIsNonEmptyArray,
|
|
169
|
+
runFeatureDevPrecheck,
|
|
170
|
+
spurCommand,
|
|
171
|
+
};
|