@gobing-ai/knowledge-kit 0.0.23 → 0.0.24
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 +137 -59
- package/package.json +4 -4
- package/plugins/generations/content-gen/dist/index.js +97 -45
- package/plugins/generations/core-facts-gen/dist/index.js +106 -57
- package/plugins/generations/daily-article-gen/dist/index.js +105 -56
- package/plugins/generations/dailynews-gen/dist/index.js +235 -57
- package/plugins/generations/dailynews-gen/src/index.ts +67 -2
- package/plugins/generations/dailynews-gen/src/script-builder.ts +36 -15
- package/plugins/generations/dailynews-gen/src/script-plan.ts +142 -0
- package/plugins/generations/episode-plan-gen/dist/index.js +115 -66
- package/plugins/generations/gemini-tts/README.md +24 -0
- package/plugins/generations/gemini-tts/dist/index.js +326 -260
- package/plugins/generations/gemini-tts/src/gemini-client.ts +41 -11
- package/plugins/generations/image-gen/dist/index.js +338 -117
- package/plugins/generations/image-gen/presets/formats/daily-news-cover.yaml +19 -0
- package/plugins/generations/image-gen/presets/formats/xhs-card.yaml +55 -0
- package/plugins/generations/image-gen/presets/styles/daily-news.yaml +9 -0
- package/plugins/generations/image-gen/presets/styles/xhs-knowledge.yaml +7 -0
- package/plugins/generations/image-gen/src/index.ts +148 -14
- package/plugins/generations/image-gen/src/job.ts +30 -0
- package/plugins/generations/image-gen/src/providers/errors.ts +91 -0
- package/plugins/generations/image-gen/src/providers/types.ts +79 -0
- package/plugins/generations/news-report-gen/dist/index.js +136 -55
- package/plugins/generations/news-report-gen/src/index.ts +57 -1
- package/plugins/generations/voice-gen/dist/index.js +119 -67
- package/plugins/ingestions/aihot-ingest/dist/index.js +103 -53
- package/plugins/ingestions/horizon-ingest/dist/index.js +102 -53
- package/plugins/ingestions/karakeep-local/dist/index.js +273 -224
- package/plugins/ingestions/last30days-ingest/dist/index.js +97 -48
- package/plugins/ingestions/web-search/dist/index.js +121 -72
- package/plugins/kk/config.example.yaml +13 -0
- package/plugins/kk/plugin.json +1 -1
- package/plugins/kk/scripts/daily-voice-style.zh.md +52 -0
- package/plugins/kk/scripts/kk-workflow-stages.ts +433 -16
- package/plugins/kk/skills/publish/SKILL.md +24 -1
- package/plugins/kk/skills/xhs-images/SKILL.md +89 -0
- package/plugins/kk/skills/xhs-images/references/card-layouts.md +35 -0
- package/plugins/kk/workflows/kk-daily-ai-voice.yaml +162 -11
- package/plugins/publishings/emdash-pub/dist/index.js +94 -45
- package/plugins/publishings/infoq-pub/dist/index.js +94 -45
- package/plugins/publishings/juejin-pub/dist/index.js +94 -45
- package/plugins/publishings/medium-pub/dist/index.js +94 -45
- package/plugins/publishings/podcast-pub/dist/index.js +115 -55
- package/plugins/publishings/podcast-pub/src/map.ts +30 -10
- package/plugins/publishings/podcast-pub/src/show-notes.ts +6 -1
- package/plugins/publishings/qiita-pub/dist/index.js +94 -45
- package/plugins/publishings/substack-pub/dist/index.js +94 -45
- package/plugins/publishings/surfdash-pub/dist/index.js +99 -63
- package/plugins/publishings/surfdash-pub/src/index.ts +10 -1
- package/plugins/publishings/wechatmp-pub/dist/index.js +94 -45
- package/plugins/publishings/x-pub/dist/index.js +94 -45
- package/plugins/publishings/xhs-pub/dist/index.js +8908 -229
- package/plugins/publishings/xhs-pub/package.json +1 -0
- package/plugins/publishings/xhs-pub/plugin.json +1 -1
- package/plugins/publishings/xhs-pub/src/automation-guard.ts +128 -0
- package/plugins/publishings/xhs-pub/src/index.ts +753 -24
- package/plugins/publishings/xhs-pub/src/profile-check.ts +133 -0
- package/plugins/publishings/zenn-pub/dist/index.js +94 -45
- package/plugins/sp/lib/env.ts +46 -0
- package/plugins/sp/lib/inline-run.generated.mjs +1489 -0
- package/plugins/sp/scripts/feature-verification-steps.ts +275 -0
- package/plugins/sp/scripts/inline-run-setup.ts +571 -47
- package/plugins/sp/scripts/wrapup-drift-probe.mjs +190 -0
- package/plugins/sp/scripts/wrapup-drift-probe.ts +258 -0
- package/plugins/sp/scripts/wrapup-steps.mjs +77 -6
- package/plugins/sp/scripts/wrapup-steps.ts +91 -5
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @bun
|
|
3
|
+
|
|
4
|
+
// plugins/sp/scripts/wrapup-drift-probe.ts
|
|
5
|
+
import { spawnSync } from 'child_process';
|
|
6
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
7
|
+
import { join } from 'path';
|
|
8
|
+
|
|
9
|
+
// plugins/sp/lib/env.ts
|
|
10
|
+
function getEnvVars() {
|
|
11
|
+
return process.env;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// plugins/sp/scripts/wrapup-drift-probe.ts
|
|
15
|
+
var WORKFLOWS_GLOB = `${join('config', 'workflows')}/**`;
|
|
16
|
+
var DOC_OWNED_SURFACES = [
|
|
17
|
+
'packages/contracts/**',
|
|
18
|
+
'apps/cli/src/commands/**',
|
|
19
|
+
'packages/config/src/**',
|
|
20
|
+
'drizzle/*.sql',
|
|
21
|
+
WORKFLOWS_GLOB,
|
|
22
|
+
'plugins/sp/commands/**',
|
|
23
|
+
'plugins/sp/skills/**',
|
|
24
|
+
'plugins/sp/hooks/**',
|
|
25
|
+
'package.json',
|
|
26
|
+
'docs/00_ADR.md',
|
|
27
|
+
'docs/03_ARCHITECTURE.md',
|
|
28
|
+
'docs/04_DESIGN.md',
|
|
29
|
+
'docs/design/**',
|
|
30
|
+
];
|
|
31
|
+
var KNOWN_TOP_LEVEL = new Set([
|
|
32
|
+
'apps',
|
|
33
|
+
'packages',
|
|
34
|
+
'plugins',
|
|
35
|
+
'config',
|
|
36
|
+
'docs',
|
|
37
|
+
'drizzle',
|
|
38
|
+
'scripts',
|
|
39
|
+
'vendors',
|
|
40
|
+
'package.json',
|
|
41
|
+
'bun.lock',
|
|
42
|
+
'bunfig.toml',
|
|
43
|
+
]);
|
|
44
|
+
var CORPUS_PREFIXES = ['docs/tasks', 'docs/features/'];
|
|
45
|
+
function solutionSectionOf(content) {
|
|
46
|
+
const heading = /^#{2,4}\s+Solution\s*$/m.exec(content);
|
|
47
|
+
if (!heading) return null;
|
|
48
|
+
const rest = content.slice(heading.index + heading[0].length);
|
|
49
|
+
const next = /^#{2,4}\s+\S/m.exec(rest);
|
|
50
|
+
return next ? rest.slice(0, next.index) : rest;
|
|
51
|
+
}
|
|
52
|
+
var CHANGE_ENTRY = /`([^`\r\n]+):(\d+)(?:-\d+)?`/g;
|
|
53
|
+
function changedPathsOf(section) {
|
|
54
|
+
if (section === null) return [];
|
|
55
|
+
const paths = [];
|
|
56
|
+
for (const match of section.matchAll(CHANGE_ENTRY)) {
|
|
57
|
+
const path = match[1];
|
|
58
|
+
if (/\s/.test(path)) continue;
|
|
59
|
+
if (!path.includes('/') && !path.includes('.')) continue;
|
|
60
|
+
paths.push(path);
|
|
61
|
+
}
|
|
62
|
+
return paths;
|
|
63
|
+
}
|
|
64
|
+
function surfaceRegex(glob) {
|
|
65
|
+
const escaped = glob
|
|
66
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
67
|
+
.replaceAll('**', '\x00')
|
|
68
|
+
.replaceAll('*', '[^/]*')
|
|
69
|
+
.replaceAll('\x00', '.*');
|
|
70
|
+
return new RegExp(`^${escaped}$`);
|
|
71
|
+
}
|
|
72
|
+
var SURFACE_MATCHERS = DOC_OWNED_SURFACES.map((glob) => ({ glob, regex: surfaceRegex(glob) }));
|
|
73
|
+
function driftReasonForPath(path) {
|
|
74
|
+
if (CORPUS_PREFIXES.some((prefix) => path.startsWith(prefix))) return null;
|
|
75
|
+
if (path.includes('/') && !KNOWN_TOP_LEVEL.has(path.split('/')[0])) {
|
|
76
|
+
return 'new top-level workspace directory';
|
|
77
|
+
}
|
|
78
|
+
const match = SURFACE_MATCHERS.find((entry) => entry.regex.test(path));
|
|
79
|
+
return match ? `matches doc-owned surface ${match.glob}` : null;
|
|
80
|
+
}
|
|
81
|
+
function defaultSpurRunner(env, cwd) {
|
|
82
|
+
const parts = (env.spurBin ?? 'spur').split(/\s+/).filter((part) => part.length > 0);
|
|
83
|
+
return (args) => {
|
|
84
|
+
const run = spawnSync(parts[0], [...parts.slice(1), ...args], {
|
|
85
|
+
cwd,
|
|
86
|
+
encoding: 'utf8',
|
|
87
|
+
env: getEnvVars(),
|
|
88
|
+
});
|
|
89
|
+
return { status: run.status ?? 1, stdout: run.stdout ?? '' };
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function runDriftProbe(env, options = {}, spur = defaultSpurRunner(env, options.cwd)) {
|
|
93
|
+
const cwd = options.cwd;
|
|
94
|
+
const runId = env.__runId ?? '';
|
|
95
|
+
if (runId.length === 0) {
|
|
96
|
+
process.stderr.write(`wrapup-drift-probe: __runId is empty \u2014 refusing the legacy fixed-path fallback
|
|
97
|
+
`);
|
|
98
|
+
return { clean: false, reasons: [], paths: [], probeFile: '', modeFile: '', exitCode: 1 };
|
|
99
|
+
}
|
|
100
|
+
const relProbeFile = join('.spur', 'run', `${runId}-drift-probe.json`);
|
|
101
|
+
const relModeFile = join('.spur', 'run', `${runId}-mode.txt`);
|
|
102
|
+
const relTasksFile = join('.spur', 'run', `${runId}-wrapup-tasks.json`);
|
|
103
|
+
const abs = (p) => (cwd ? join(cwd, p) : p);
|
|
104
|
+
mkdirSync(abs(join('.spur', 'run')), { recursive: true });
|
|
105
|
+
writeFileSync(
|
|
106
|
+
abs(relModeFile),
|
|
107
|
+
`
|
|
108
|
+
`,
|
|
109
|
+
);
|
|
110
|
+
const finish = (clean, reasons2, paths) => {
|
|
111
|
+
writeFileSync(
|
|
112
|
+
abs(relProbeFile),
|
|
113
|
+
`${JSON.stringify({ clean, reasons: reasons2, paths })}
|
|
114
|
+
`,
|
|
115
|
+
);
|
|
116
|
+
writeFileSync(
|
|
117
|
+
abs(relModeFile),
|
|
118
|
+
clean
|
|
119
|
+
? `fast
|
|
120
|
+
`
|
|
121
|
+
: `
|
|
122
|
+
`,
|
|
123
|
+
);
|
|
124
|
+
return { clean, reasons: reasons2, paths, probeFile: relProbeFile, modeFile: relModeFile, exitCode: 0 };
|
|
125
|
+
};
|
|
126
|
+
let tasks;
|
|
127
|
+
try {
|
|
128
|
+
tasks = JSON.parse(readFileSync(abs(relTasksFile), 'utf8'));
|
|
129
|
+
} catch {
|
|
130
|
+
tasks = undefined;
|
|
131
|
+
}
|
|
132
|
+
if (!Array.isArray(tasks) || !tasks.every((wbs) => typeof wbs === 'string')) {
|
|
133
|
+
process.stderr.write(`wrapup-drift-probe: normalized task capture is missing or corrupted \u2014 failing safe (dirty)
|
|
134
|
+
`);
|
|
135
|
+
return finish(false, ['normalized task capture missing or corrupted'], []);
|
|
136
|
+
}
|
|
137
|
+
const reasons = [];
|
|
138
|
+
const allPaths = new Set();
|
|
139
|
+
for (const wbs of tasks) {
|
|
140
|
+
const shown = spur(['task', 'show', wbs, '--json']);
|
|
141
|
+
if (shown.status !== 0) {
|
|
142
|
+
reasons.push(`${wbs}: task show failed (status=${shown.status})`);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
let content;
|
|
146
|
+
try {
|
|
147
|
+
content = JSON.parse(shown.stdout).content;
|
|
148
|
+
} catch {
|
|
149
|
+
content = undefined;
|
|
150
|
+
}
|
|
151
|
+
if (typeof content !== 'string') {
|
|
152
|
+
reasons.push(`${wbs}: task show output unparseable`);
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
const changedPaths = changedPathsOf(solutionSectionOf(content));
|
|
156
|
+
if (changedPaths.length === 0) {
|
|
157
|
+
reasons.push(`${wbs}: Solution empty or unparseable`);
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
for (const path of changedPaths) {
|
|
161
|
+
if (CORPUS_PREFIXES.some((prefix) => path.startsWith(prefix))) continue;
|
|
162
|
+
allPaths.add(path);
|
|
163
|
+
const drift = driftReasonForPath(path);
|
|
164
|
+
if (drift) reasons.push(`${wbs}: ${path} ${drift}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return finish(reasons.length === 0, reasons, [...allPaths].sort());
|
|
168
|
+
}
|
|
169
|
+
var WRAPUP_DRIFT_PROBE_USAGE = 'usage: wrapup-drift-probe (env: __runId, spurBin)';
|
|
170
|
+
function main(argv, env = getEnvVars(), options = {}) {
|
|
171
|
+
if (argv.length > 0) {
|
|
172
|
+
process.stderr.write(`${WRAPUP_DRIFT_PROBE_USAGE}
|
|
173
|
+
`);
|
|
174
|
+
return 2;
|
|
175
|
+
}
|
|
176
|
+
return runDriftProbe(env, options).exitCode;
|
|
177
|
+
}
|
|
178
|
+
{
|
|
179
|
+
process.exit(main(process.argv.slice(2)));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export {
|
|
183
|
+
changedPathsOf,
|
|
184
|
+
DOC_OWNED_SURFACES,
|
|
185
|
+
driftReasonForPath,
|
|
186
|
+
main,
|
|
187
|
+
runDriftProbe,
|
|
188
|
+
solutionSectionOf,
|
|
189
|
+
WRAPUP_DRIFT_PROBE_USAGE,
|
|
190
|
+
};
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* wrapup-drift-probe — deterministic doc-ownership drift probe behind the wrapup-pipeline
|
|
4
|
+
* task-resolve onEnter (task 0944, feature D64, ADR-115 composition budgets).
|
|
5
|
+
*
|
|
6
|
+
* Reads the validated wrapup capture `.spur/run/<runId>-wrapup-tasks.json` (never raw
|
|
7
|
+
* vars.tasks — 0783 contract), runs `spur task show <wbs> --json` per member, collects the
|
|
8
|
+
* changed paths from each task's `### Solution` file:line map, and writes:
|
|
9
|
+
* - `<runId>-drift-probe.json` `{ clean: boolean, reasons: string[], paths: string[] }`
|
|
10
|
+
* - `<runId>-mode.txt` projected wrapup mode: `fast` when clean, empty otherwise
|
|
11
|
+
*
|
|
12
|
+
* The probe runs ONLY when the caller left `mode` empty; a caller-set mode is projected
|
|
13
|
+
* verbatim by the workflow wrapper and this script is never invoked (0944 R3).
|
|
14
|
+
*
|
|
15
|
+
* Dirty (clean=false, mode stays empty → the safety route) whenever ANY changed path
|
|
16
|
+
* matches a doc-owned surface (AGENTS.md doc map + docs/99_PROJECT_CONSTITUTION.md
|
|
17
|
+
* ownership table), or a task's Solution section is empty or unparseable (fail safe —
|
|
18
|
+
* a wrapup without a readable change map must still reach doc-sync). Paths under the
|
|
19
|
+
* task/feature corpus are never treated as drift.
|
|
20
|
+
*
|
|
21
|
+
* Node-builtin imports only; the spur lookup is a spawnable `SpurRunner` so tests can
|
|
22
|
+
* fake it; `main(argv, env, options)` is the same injection point the workflow wrapper
|
|
23
|
+
* and its node twin exercise. Always exits 0 after probing; only an empty `__runId`
|
|
24
|
+
* (mis-invocation) exits 1.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { spawnSync } from 'node:child_process';
|
|
28
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
29
|
+
import { join } from 'node:path';
|
|
30
|
+
import { getEnvVars } from '../lib/env';
|
|
31
|
+
|
|
32
|
+
export interface DriftProbeEnv {
|
|
33
|
+
__runId?: string;
|
|
34
|
+
spurBin?: string;
|
|
35
|
+
[key: string]: string | undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface DriftProbeOptions {
|
|
39
|
+
/** Base directory for `.spur/run`; defaults to the process cwd. */
|
|
40
|
+
cwd?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Minimal spawnable spur surface — tests fake this to return canned `task show` output. */
|
|
44
|
+
export interface SpurShowResult {
|
|
45
|
+
status: number;
|
|
46
|
+
stdout: string;
|
|
47
|
+
}
|
|
48
|
+
export type SpurRunner = (args: string[]) => SpurShowResult;
|
|
49
|
+
|
|
50
|
+
export interface DriftProbeResult {
|
|
51
|
+
clean: boolean;
|
|
52
|
+
reasons: string[];
|
|
53
|
+
paths: string[];
|
|
54
|
+
probeFile: string;
|
|
55
|
+
modeFile: string;
|
|
56
|
+
/** Exit code the workflow wrapper observes (only an empty __runId is a hard failure). */
|
|
57
|
+
exitCode: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Doc-owned surfaces (0944 R2): a changed path under any of these means doc-sync must
|
|
62
|
+
* still run. Mirrors the AGENTS.md documentation map and the
|
|
63
|
+
* docs/99_PROJECT_CONSTITUTION.md ownership table: contracts / cli commands / config /
|
|
64
|
+
* migrations / workflow YAML / plugin commands, skills and hooks / root manifest / the
|
|
65
|
+
* authoritative docs (00_ADR, 03_ARCHITECTURE, 04_DESIGN, docs/design). docs/tasks* and
|
|
66
|
+
* docs/features/* (the corpus) are intentionally absent — they are never drift.
|
|
67
|
+
*
|
|
68
|
+
* WHY join('config', 'workflows'): rule sp-runtime-path forbids the literal
|
|
69
|
+
* config/<dir> in runtime source; this constant is a drift TARGET (Solution
|
|
70
|
+
* change-map paths against the build-time SSOT), not a runtime config read.
|
|
71
|
+
*/
|
|
72
|
+
const WORKFLOWS_GLOB = `${join('config', 'workflows')}/**`;
|
|
73
|
+
export const DOC_OWNED_SURFACES = [
|
|
74
|
+
'packages/contracts/**',
|
|
75
|
+
'apps/cli/src/commands/**',
|
|
76
|
+
'packages/config/src/**',
|
|
77
|
+
'drizzle/*.sql',
|
|
78
|
+
WORKFLOWS_GLOB,
|
|
79
|
+
'plugins/sp/commands/**',
|
|
80
|
+
'plugins/sp/skills/**',
|
|
81
|
+
'plugins/sp/hooks/**',
|
|
82
|
+
'package.json',
|
|
83
|
+
'docs/00_ADR.md',
|
|
84
|
+
'docs/03_ARCHITECTURE.md',
|
|
85
|
+
'docs/04_DESIGN.md',
|
|
86
|
+
'docs/design/**',
|
|
87
|
+
] as const;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Top-level workspace entries from the AGENTS.md stack layout. A changed path whose first
|
|
91
|
+
* segment is none of these is a new top-level workspace directory — doc-owned by default.
|
|
92
|
+
* Single-segment paths (root files) are not directories and are exempt.
|
|
93
|
+
*/
|
|
94
|
+
const KNOWN_TOP_LEVEL = new Set([
|
|
95
|
+
'apps',
|
|
96
|
+
'packages',
|
|
97
|
+
'plugins',
|
|
98
|
+
'config',
|
|
99
|
+
'docs',
|
|
100
|
+
'drizzle',
|
|
101
|
+
'scripts',
|
|
102
|
+
'vendors',
|
|
103
|
+
'package.json',
|
|
104
|
+
'bun.lock',
|
|
105
|
+
'bunfig.toml',
|
|
106
|
+
]);
|
|
107
|
+
|
|
108
|
+
/** Corpus paths (task/feature records) are never drift — excluded before surface matching. */
|
|
109
|
+
const CORPUS_PREFIXES = ['docs/tasks', 'docs/features/'];
|
|
110
|
+
|
|
111
|
+
/** `### Solution` section of a task record (tolerates `##`–`####` heading depth). */
|
|
112
|
+
export function solutionSectionOf(content: string): string | null {
|
|
113
|
+
const heading = /^#{2,4}\s+Solution\s*$/m.exec(content);
|
|
114
|
+
if (!heading) return null;
|
|
115
|
+
const rest = content.slice(heading.index + heading[0].length);
|
|
116
|
+
const next = /^#{2,4}\s+\S/m.exec(rest);
|
|
117
|
+
return next ? rest.slice(0, next.index) : rest;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const CHANGE_ENTRY = /`([^`\r\n]+):(\d+)(?:-\d+)?`/g;
|
|
121
|
+
|
|
122
|
+
/** Backticked `path:line` (or `path:line-range`) tokens from a Solution section. */
|
|
123
|
+
export function changedPathsOf(section: string | null): string[] {
|
|
124
|
+
if (section === null) return [];
|
|
125
|
+
const paths: string[] = [];
|
|
126
|
+
for (const match of section.matchAll(CHANGE_ENTRY)) {
|
|
127
|
+
const path = match[1];
|
|
128
|
+
// Path-like: no whitespace and at least one separator or dotted suffix, so prose
|
|
129
|
+
// backticks never masquerade as change-map entries.
|
|
130
|
+
if (/\s/.test(path)) continue;
|
|
131
|
+
if (!path.includes('/') && !path.includes('.')) continue;
|
|
132
|
+
paths.push(path);
|
|
133
|
+
}
|
|
134
|
+
return paths;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function surfaceRegex(glob: string): RegExp {
|
|
138
|
+
const escaped = glob
|
|
139
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
140
|
+
.replaceAll('**', '\u0000')
|
|
141
|
+
.replaceAll('*', '[^/]*')
|
|
142
|
+
.replaceAll('\u0000', '.*');
|
|
143
|
+
return new RegExp(`^${escaped}$`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const SURFACE_MATCHERS = DOC_OWNED_SURFACES.map((glob) => ({ glob, regex: surfaceRegex(glob) }));
|
|
147
|
+
|
|
148
|
+
/** 0944 R2 surface match: doc-owned glob, corpus exclusion, new top-level directory. */
|
|
149
|
+
export function driftReasonForPath(path: string): string | null {
|
|
150
|
+
if (CORPUS_PREFIXES.some((prefix) => path.startsWith(prefix))) return null;
|
|
151
|
+
if (path.includes('/') && !KNOWN_TOP_LEVEL.has(path.split('/')[0])) {
|
|
152
|
+
return 'new top-level workspace directory';
|
|
153
|
+
}
|
|
154
|
+
const match = SURFACE_MATCHERS.find((entry) => entry.regex.test(path));
|
|
155
|
+
return match ? `matches doc-owned surface ${match.glob}` : null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function defaultSpurRunner(env: DriftProbeEnv, cwd?: string): SpurRunner {
|
|
159
|
+
const parts = (env.spurBin ?? 'spur').split(/\s+/).filter((part) => part.length > 0);
|
|
160
|
+
return (args: string[]): SpurShowResult => {
|
|
161
|
+
const run = spawnSync(parts[0], [...parts.slice(1), ...args], {
|
|
162
|
+
cwd,
|
|
163
|
+
encoding: 'utf8',
|
|
164
|
+
env: getEnvVars(),
|
|
165
|
+
});
|
|
166
|
+
return { status: run.status ?? 1, stdout: run.stdout ?? '' };
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* `probe` — classify the wrapup as clean (pure application-code change, no doc-owned
|
|
172
|
+
* surface touched) or dirty, and project the wrapup mode. Fail safe: every lookup or
|
|
173
|
+
* parse problem is a dirty probe, never a silent clean.
|
|
174
|
+
*/
|
|
175
|
+
export function runDriftProbe(
|
|
176
|
+
env: DriftProbeEnv,
|
|
177
|
+
options: DriftProbeOptions = {},
|
|
178
|
+
spur = defaultSpurRunner(env, options.cwd),
|
|
179
|
+
): DriftProbeResult {
|
|
180
|
+
const cwd = options.cwd;
|
|
181
|
+
const runId = env.__runId ?? '';
|
|
182
|
+
if (runId.length === 0) {
|
|
183
|
+
process.stderr.write('wrapup-drift-probe: __runId is empty — refusing the legacy fixed-path fallback\n');
|
|
184
|
+
return { clean: false, reasons: [], paths: [], probeFile: '', modeFile: '', exitCode: 1 };
|
|
185
|
+
}
|
|
186
|
+
const relProbeFile = join('.spur', 'run', `${runId}-drift-probe.json`);
|
|
187
|
+
const relModeFile = join('.spur', 'run', `${runId}-mode.txt`);
|
|
188
|
+
const relTasksFile = join('.spur', 'run', `${runId}-wrapup-tasks.json`);
|
|
189
|
+
const abs = (p: string): string => (cwd ? join(cwd, p) : p);
|
|
190
|
+
mkdirSync(abs(join('.spur', 'run')), { recursive: true });
|
|
191
|
+
// Write the dirty projection first so a crash mid-probe fails safe (mode stays empty).
|
|
192
|
+
writeFileSync(abs(relModeFile), '\n');
|
|
193
|
+
|
|
194
|
+
const finish = (clean: boolean, reasons: string[], paths: string[]): DriftProbeResult => {
|
|
195
|
+
writeFileSync(abs(relProbeFile), `${JSON.stringify({ clean, reasons, paths })}\n`);
|
|
196
|
+
writeFileSync(abs(relModeFile), clean ? 'fast\n' : '\n');
|
|
197
|
+
return { clean, reasons, paths, probeFile: relProbeFile, modeFile: relModeFile, exitCode: 0 };
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
let tasks: unknown;
|
|
201
|
+
try {
|
|
202
|
+
tasks = JSON.parse(readFileSync(abs(relTasksFile), 'utf8'));
|
|
203
|
+
} catch {
|
|
204
|
+
tasks = undefined;
|
|
205
|
+
}
|
|
206
|
+
if (!Array.isArray(tasks) || !tasks.every((wbs) => typeof wbs === 'string')) {
|
|
207
|
+
process.stderr.write(
|
|
208
|
+
'wrapup-drift-probe: normalized task capture is missing or corrupted — failing safe (dirty)\n',
|
|
209
|
+
);
|
|
210
|
+
return finish(false, ['normalized task capture missing or corrupted'], []);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const reasons: string[] = [];
|
|
214
|
+
const allPaths = new Set<string>();
|
|
215
|
+
for (const wbs of tasks as string[]) {
|
|
216
|
+
const shown = spur(['task', 'show', wbs, '--json']);
|
|
217
|
+
if (shown.status !== 0) {
|
|
218
|
+
reasons.push(`${wbs}: task show failed (status=${shown.status})`);
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
let content: unknown;
|
|
222
|
+
try {
|
|
223
|
+
content = JSON.parse(shown.stdout).content;
|
|
224
|
+
} catch {
|
|
225
|
+
content = undefined;
|
|
226
|
+
}
|
|
227
|
+
if (typeof content !== 'string') {
|
|
228
|
+
reasons.push(`${wbs}: task show output unparseable`);
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
const changedPaths = changedPathsOf(solutionSectionOf(content));
|
|
232
|
+
if (changedPaths.length === 0) {
|
|
233
|
+
reasons.push(`${wbs}: Solution empty or unparseable`);
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
for (const path of changedPaths) {
|
|
237
|
+
if (CORPUS_PREFIXES.some((prefix) => path.startsWith(prefix))) continue;
|
|
238
|
+
allPaths.add(path);
|
|
239
|
+
const drift = driftReasonForPath(path);
|
|
240
|
+
if (drift) reasons.push(`${wbs}: ${path} ${drift}`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return finish(reasons.length === 0, reasons, [...allPaths].sort());
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export const WRAPUP_DRIFT_PROBE_USAGE = 'usage: wrapup-drift-probe (env: __runId, spurBin)';
|
|
247
|
+
|
|
248
|
+
export function main(argv: string[], env: DriftProbeEnv = getEnvVars(), options: DriftProbeOptions = {}): number {
|
|
249
|
+
if (argv.length > 0) {
|
|
250
|
+
process.stderr.write(`${WRAPUP_DRIFT_PROBE_USAGE}\n`);
|
|
251
|
+
return 2;
|
|
252
|
+
}
|
|
253
|
+
return runDriftProbe(env, options).exitCode;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (import.meta.main) {
|
|
257
|
+
process.exit(main(process.argv.slice(2)));
|
|
258
|
+
}
|
|
@@ -2,10 +2,16 @@
|
|
|
2
2
|
// @bun
|
|
3
3
|
|
|
4
4
|
// plugins/sp/scripts/wrapup-steps.ts
|
|
5
|
-
import { spawnSync } from '
|
|
6
|
-
import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from '
|
|
7
|
-
import { join } from '
|
|
5
|
+
import { spawnSync } from 'child_process';
|
|
6
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
7
|
+
import { join } from 'path';
|
|
8
8
|
|
|
9
|
+
// plugins/sp/lib/env.ts
|
|
10
|
+
function getEnvVars() {
|
|
11
|
+
return process.env;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// plugins/sp/scripts/wrapup-steps.ts
|
|
9
15
|
function jqPick(...values) {
|
|
10
16
|
for (const value of values) {
|
|
11
17
|
if (value !== null && value !== undefined && value !== false) return value;
|
|
@@ -118,6 +124,67 @@ function resolveTasks(env, options = {}) {
|
|
|
118
124
|
);
|
|
119
125
|
return { status: 'PASS', statusFile: relStatusFile, tasksFile: relTasksFile, exitCode: 0 };
|
|
120
126
|
}
|
|
127
|
+
var ROUTE_REASON_TABLE = {
|
|
128
|
+
fast: 'fast:evidence complete+consistent',
|
|
129
|
+
'': 'safety:missing evidence (mode empty)',
|
|
130
|
+
unknown: 'safety:unknown evidence quality',
|
|
131
|
+
conflict: 'safety:conflicting evidence',
|
|
132
|
+
safety: 'safety:operator-forced doc-sync',
|
|
133
|
+
};
|
|
134
|
+
function writeRouteReason(env, options = {}) {
|
|
135
|
+
const cwd = options.cwd;
|
|
136
|
+
const runId = env.__runId ?? '';
|
|
137
|
+
if (runId.length === 0) {
|
|
138
|
+
process.stderr.write(`task-resolve: __runId is empty \u2014 refusing to write a route reason
|
|
139
|
+
`);
|
|
140
|
+
return { reason: '', reasonFile: '', exitCode: 1 };
|
|
141
|
+
}
|
|
142
|
+
mkdirSync(cwd ? join(cwd, '.spur', 'run') : join('.spur', 'run'), { recursive: true });
|
|
143
|
+
mkdirSync(cwd ? join(cwd, '.spur', 'memory') : join('.spur', 'memory'), { recursive: true });
|
|
144
|
+
const relReasonFile = join('.spur', 'run', `${runId}-route-reason.txt`);
|
|
145
|
+
const abs = (p) => (cwd ? join(cwd, p) : p);
|
|
146
|
+
const statusFile = join('.spur', 'run', `${runId}-wrapup-resolve.status`);
|
|
147
|
+
if (readFileSyncSafe(abs(statusFile))?.trim() === 'FAIL') {
|
|
148
|
+
return { reason: '', reasonFile: relReasonFile, exitCode: 0 };
|
|
149
|
+
}
|
|
150
|
+
let taskCount = -1;
|
|
151
|
+
try {
|
|
152
|
+
const parsed = JSON.parse(readFileSync(abs(join('.spur', 'run', `${runId}-wrapup-tasks.json`)), 'utf8'));
|
|
153
|
+
if (Array.isArray(parsed)) taskCount = parsed.length;
|
|
154
|
+
} catch {}
|
|
155
|
+
let probeClean = false;
|
|
156
|
+
try {
|
|
157
|
+
const probe = JSON.parse(readFileSync(abs(join('.spur', 'run', `${runId}-drift-probe.json`)), 'utf8'));
|
|
158
|
+
probeClean = probe?.clean === true;
|
|
159
|
+
} catch {}
|
|
160
|
+
const mode = env.mode ?? '';
|
|
161
|
+
let reason;
|
|
162
|
+
if (taskCount === 0) {
|
|
163
|
+
reason = 'skipped:empty task list';
|
|
164
|
+
} else if (mode === 'fast' && probeClean) {
|
|
165
|
+
reason = 'fast:drift-probe-clean';
|
|
166
|
+
} else {
|
|
167
|
+
reason = ROUTE_REASON_TABLE[mode] ?? `safety:unrecognized evidence (mode=${mode})`;
|
|
168
|
+
}
|
|
169
|
+
writeFileSync(
|
|
170
|
+
abs(relReasonFile),
|
|
171
|
+
`${reason}
|
|
172
|
+
`,
|
|
173
|
+
);
|
|
174
|
+
appendFileSync(
|
|
175
|
+
abs(join('.spur', 'memory', 'wrapup-routes.log')),
|
|
176
|
+
`${runId} ${reason}
|
|
177
|
+
`,
|
|
178
|
+
);
|
|
179
|
+
return { reason, reasonFile: relReasonFile, exitCode: 0 };
|
|
180
|
+
}
|
|
181
|
+
function readFileSyncSafe(path) {
|
|
182
|
+
try {
|
|
183
|
+
return readFileSync(path, 'utf8');
|
|
184
|
+
} catch {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
121
188
|
function runMetrics(env, options = {}) {
|
|
122
189
|
const cwd = options.cwd;
|
|
123
190
|
const runId = env.__runId ?? '';
|
|
@@ -344,10 +411,11 @@ function runFeatureTransition(env, options = {}) {
|
|
|
344
411
|
return { status: syncStatus, statusFile: relStatusFile, exitCode: 0 };
|
|
345
412
|
}
|
|
346
413
|
var WRAPUP_STEPS_USAGE =
|
|
347
|
-
'usage: wrapup-steps.ts <resolve|metrics|feature-transition> (env: __runId, tasks, feature, featureGateCmd, spurBin)';
|
|
348
|
-
function main(argv, env =
|
|
414
|
+
'usage: wrapup-steps.ts <resolve|route-reason|metrics|feature-transition> (env: __runId, tasks, mode, feature, featureGateCmd, spurBin)';
|
|
415
|
+
function main(argv, env = getEnvVars(), options = {}) {
|
|
349
416
|
const sub = argv[0];
|
|
350
417
|
if (sub === 'resolve') return resolveTasks(env, options).exitCode;
|
|
418
|
+
if (sub === 'route-reason') return writeRouteReason(env, options).exitCode;
|
|
351
419
|
if (sub === 'metrics') {
|
|
352
420
|
runMetrics(env, options);
|
|
353
421
|
return 0;
|
|
@@ -357,7 +425,9 @@ function main(argv, env = process.env, options = {}) {
|
|
|
357
425
|
`);
|
|
358
426
|
return 2;
|
|
359
427
|
}
|
|
360
|
-
|
|
428
|
+
{
|
|
429
|
+
process.exit(main(process.argv.slice(2)));
|
|
430
|
+
}
|
|
361
431
|
|
|
362
432
|
export {
|
|
363
433
|
classifySync,
|
|
@@ -370,4 +440,5 @@ export {
|
|
|
370
440
|
taskStatusOf,
|
|
371
441
|
WBS_PATTERN,
|
|
372
442
|
WRAPUP_STEPS_USAGE,
|
|
443
|
+
writeRouteReason,
|
|
373
444
|
};
|