@ericrisco/rsc 1.0.8 → 1.0.10
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/manifest.json +1 -1
- package/package.json +4 -2
- package/scripts/drift-check.js +316 -0
- package/scripts/skill-harden-rubric.md +1 -1
- package/skills/data-cleaning/references/normalization-recipes.md +1 -1
- package/skills/harness/references/wiki-gaps-template.md +4 -0
- package/skills/solid-js/references/router-and-start.md +1 -1
package/manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ericrisco/rsc",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.10",
|
|
4
4
|
"description": "Eric Risco's agent-skills catalog as a granular, self-recommending CLI installer.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -14,10 +14,12 @@
|
|
|
14
14
|
"manifest": "node scripts/build-manifest.js",
|
|
15
15
|
"manifest:check": "node scripts/build-manifest.js --check",
|
|
16
16
|
"validate": "node scripts/build-manifest.js --validate",
|
|
17
|
+
"drift:check": "node scripts/drift-check.js",
|
|
18
|
+
"drift:knowledge": "node scripts/drift-check.js --knowledge",
|
|
17
19
|
"test": "node --test",
|
|
18
20
|
"coverage": "c8 --reporter=text node --test",
|
|
19
21
|
"version": "npm run manifest && git add manifest.json",
|
|
20
|
-
"prepublishOnly": "npm run manifest && npm run validate"
|
|
22
|
+
"prepublishOnly": "npm run manifest && npm run validate && npm run drift:check"
|
|
21
23
|
},
|
|
22
24
|
"dependencies": {
|
|
23
25
|
"@iarna/toml": "^2.2.5",
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// drift-check.js — do the paths the harness claims exist actually exist?
|
|
3
|
+
//
|
|
4
|
+
// The harness writes knowledge about itself in three places and every one of them can name a
|
|
5
|
+
// file that has since moved. Measured before this existed: 67 of 688 markdown links in the
|
|
6
|
+
// published catalog did not resolve, and two were real wrong-depth bugs shipping to users
|
|
7
|
+
// (`data-cleaning` → duckdb, `solid-js` → vercel, both missing a `../`). The private wiki had a
|
|
8
|
+
// plan marked "implementada" naming three files that never existed. Nothing checked any of it,
|
|
9
|
+
// while the memory instructions already ordered "verify it still exists before recommending it"
|
|
10
|
+
// — a binding rule resting on the model remembering. That is the pattern P2 exists to kill.
|
|
11
|
+
//
|
|
12
|
+
// THE ONE IDEA THAT MAKES THIS VIABLE: **code shows, prose points.** A link inside a fenced
|
|
13
|
+
// block or an inline-code span is being *displayed* as syntax; a link in prose is *offered to
|
|
14
|
+
// follow*. That distinction is already in the markup, so it needs no markers to author and no
|
|
15
|
+
// allowlist to maintain (P3 — the content is the ledger). Applied to the catalog it takes 67
|
|
16
|
+
// unresolved links down to 4 real ones, which is the difference between a gate people act on
|
|
17
|
+
// and a gate they learn to ignore.
|
|
18
|
+
//
|
|
19
|
+
// Two modes, because the precision a check needs scales with what it costs to be wrong (P7):
|
|
20
|
+
// catalog blocking. Prose links only, resolved exactly as a markdown reader would (relative
|
|
21
|
+
// to the document). Precision first: a false positive here fails a build.
|
|
22
|
+
// knowledge report-only. Prose links PLUS paths in inline code, resolved against several
|
|
23
|
+
// plausible bases. Recall first: a human triages, nothing blocks, and the wiki's
|
|
24
|
+
// worst finding lives in backticks — precision-only extraction would miss it.
|
|
25
|
+
//
|
|
26
|
+
// It never writes. The output is a report; adopting it is a human's job.
|
|
27
|
+
import { readdirSync, readFileSync, existsSync, realpathSync } from 'node:fs';
|
|
28
|
+
import { join, dirname, resolve, relative, basename } from 'node:path';
|
|
29
|
+
import { fileURLToPath } from 'node:url';
|
|
30
|
+
import { homedir } from 'node:os';
|
|
31
|
+
|
|
32
|
+
const REPO = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
33
|
+
|
|
34
|
+
// ── extraction ────────────────────────────────────────────────────────────────────────────
|
|
35
|
+
// Both strippers keep the line count (and stripInlineCode the column offsets) intact, so a
|
|
36
|
+
// finding can name the line a human has to open.
|
|
37
|
+
|
|
38
|
+
// Fenced blocks, line-scanned rather than regexed: an UNTERMINATED fence must swallow the rest
|
|
39
|
+
// of the file, which a paired regex silently declines to do — leaving example links in a
|
|
40
|
+
// half-open block looking like prose.
|
|
41
|
+
export function stripFences(text) {
|
|
42
|
+
const out = [];
|
|
43
|
+
let fence = null;
|
|
44
|
+
for (const line of String(text).split('\n')) {
|
|
45
|
+
const m = line.match(/^([ \t]*)(`{3,}|~{3,})(.*)$/);
|
|
46
|
+
if (fence) {
|
|
47
|
+
if (m && m[2][0] === fence.char && m[2].length >= fence.len && m[3].trim() === '') fence = null;
|
|
48
|
+
out.push('');
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (m) {
|
|
52
|
+
fence = { char: m[2][0], len: m[2].length };
|
|
53
|
+
out.push('');
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
out.push(line);
|
|
57
|
+
}
|
|
58
|
+
return out.join('\n');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Inline code spans, per line so a span can never eat across a newline. An UNBALANCED backtick
|
|
62
|
+
// run is left as prose on purpose: swallowing it would hide a real link, and a false negative
|
|
63
|
+
// is the failure mode this whole check exists to prevent.
|
|
64
|
+
export function stripInlineCode(text) {
|
|
65
|
+
return String(text)
|
|
66
|
+
.split('\n')
|
|
67
|
+
.map((line) => line.replace(/(`+)(?:(?!\1)[^`])*\1/g, (m) => ' '.repeat(m.length)))
|
|
68
|
+
.join('\n');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const FILE_EXT = /\.(md|markdown|ya?ml|json|jsonc|sh|bash|mjs|cjs|js|ts|txt|toml)$/i;
|
|
72
|
+
const NOT_A_PATH = /^(https?:|mailto:|tel:|#|\/\/)/i;
|
|
73
|
+
const isPathLike = (t) => !NOT_A_PATH.test(t) && (FILE_EXT.test(t) || t.endsWith('/'));
|
|
74
|
+
|
|
75
|
+
// Inline links only — `[text](target)`. Reference-style links and autolinks are deliberately
|
|
76
|
+
// out: `<path>` carries angle brackets and is classified as a placeholder anyway.
|
|
77
|
+
const LINK_RE = /\[[^\]]*\]\(\s*([^)\s]+?)\s*\)/g;
|
|
78
|
+
|
|
79
|
+
export function extractProseLinks(text) {
|
|
80
|
+
const prose = stripInlineCode(stripFences(text));
|
|
81
|
+
const out = [];
|
|
82
|
+
prose.split('\n').forEach((line, i) => {
|
|
83
|
+
for (const m of line.matchAll(LINK_RE)) {
|
|
84
|
+
const target = m[1].replace(/^<|>$/g, '');
|
|
85
|
+
if (isPathLike(target)) out.push({ target, line: i + 1, kind: 'link' });
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Report mode only: a path asserted inside backticks. Noisier by nature (this is where syntax
|
|
92
|
+
// examples live), which is exactly why it never gates a build.
|
|
93
|
+
const TOP_DIRS = /^(scripts|targets|skills|tests|bin|site|schema|02-DOCS|\.github|\.rsc)(\/|$)/;
|
|
94
|
+
export function extractInlinePaths(text) {
|
|
95
|
+
const noFences = stripFences(text);
|
|
96
|
+
const out = [];
|
|
97
|
+
noFences.split('\n').forEach((line, i) => {
|
|
98
|
+
for (const m of line.matchAll(/(`+)((?:(?!\1)[^`])*)\1/g)) {
|
|
99
|
+
const t = m[2].trim();
|
|
100
|
+
if (!t || /\s/.test(t) || NOT_A_PATH.test(t)) continue;
|
|
101
|
+
if (!/\//.test(t)) continue; // a bare filename is a mention, not a location
|
|
102
|
+
if (TOP_DIRS.test(t) || FILE_EXT.test(t)) out.push({ target: t, line: i + 1, kind: 'inline' });
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ── classification ────────────────────────────────────────────────────────────────────────
|
|
109
|
+
// Every rule is syntactic and narrow. Nothing here guesses from file names or wording: a
|
|
110
|
+
// classifier that reasons about intent is a classifier that learns to stay quiet, and a quiet
|
|
111
|
+
// detector is indistinguishable from no detector.
|
|
112
|
+
export const CLASSES = {
|
|
113
|
+
// A shape, not a path: `skills/<ID>/`, `references/*.md`, `{slug}.md`.
|
|
114
|
+
placeholder: (t) => /[<>*{}]/.test(t),
|
|
115
|
+
// The file exists; the line number moves on its own. Anchors are recognised, not chased.
|
|
116
|
+
anchored: (t) => /:\d+$/.test(t) || t.includes('#'),
|
|
117
|
+
// Born at runtime, never in a repo. Note the trailing slash: `.rsc-notes.md` is NOT this.
|
|
118
|
+
runtime: (t) => t.startsWith('~') || /(^|\/)\.rsc\//.test(t),
|
|
119
|
+
// A site route, not a file: `/llms.txt`, `/skills/`. Found by running the report on the real
|
|
120
|
+
// wiki, where six route mentions arrived as "missing files". A genuinely absolute filesystem
|
|
121
|
+
// path in these documents would start with a home or volume directory, and the P9 test in
|
|
122
|
+
// tests/drift-check.test.js already forbids shipping one.
|
|
123
|
+
'web-route': (t) => t.startsWith('/'),
|
|
124
|
+
// The INSTALLED user's project, which the catalog legitimately talks about and does not
|
|
125
|
+
// contain. Segment-exact, so `rawdata/` and `02-DOCSX/` stay real claims.
|
|
126
|
+
'user-project': (t) => /(^|\/)(02-DOCS|raw)\//.test(t),
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
export function classify(target, { mode = 'catalog' } = {}) {
|
|
130
|
+
for (const name of ['placeholder', 'anchored', 'runtime', 'web-route']) {
|
|
131
|
+
if (CLASSES[name](target)) return name;
|
|
132
|
+
}
|
|
133
|
+
// In knowledge mode this repo IS the project, so `02-DOCS/…` is a real claim about real files.
|
|
134
|
+
if (mode === 'catalog' && CLASSES['user-project'](target)) return 'user-project';
|
|
135
|
+
return 'real';
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ── resolution ────────────────────────────────────────────────────────────────────────────
|
|
139
|
+
// Catalog: the document's own directory, and nothing else — that is what a reader following the
|
|
140
|
+
// link does, and being generous here would hide the exact wrong-depth bug this must catch.
|
|
141
|
+
// Knowledge: several plausible bases, because the wiki writes shorthand
|
|
142
|
+
// (`orient/references/x.md` meaning `skills/orient/references/x.md`) and 20 such references
|
|
143
|
+
// would otherwise arrive as false positives.
|
|
144
|
+
export function basesFor(docPath, { mode, root }) {
|
|
145
|
+
if (mode === 'catalog') return [dirname(docPath)];
|
|
146
|
+
return [dirname(docPath), root, join(root, '02-DOCS'), join(root, '02-DOCS', 'wiki'), join(root, 'skills')];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const resolves = (target, bases) => bases.some((b) => existsSync(resolve(b, target)));
|
|
150
|
+
|
|
151
|
+
// Knowledge mode only. The authoring standards write per-skill paths generically — "every skill
|
|
152
|
+
// carries `evals/cases.yaml`" — which is a claim about a shape shared by 258 directories, not a
|
|
153
|
+
// location. Resolving it against any one skill is the honest reading; treating it as drift
|
|
154
|
+
// produced five findings that were never wrong.
|
|
155
|
+
function resolvesInAnySkill(target, root) {
|
|
156
|
+
if (target.startsWith('.') || target.startsWith('/')) return false;
|
|
157
|
+
const skillsDir = join(root, 'skills');
|
|
158
|
+
if (!existsSync(skillsDir)) return false;
|
|
159
|
+
for (const e of readdirSync(skillsDir, { withFileTypes: true })) {
|
|
160
|
+
if (e.isDirectory() && existsSync(join(skillsDir, e.name, target))) return true;
|
|
161
|
+
}
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ── walking ───────────────────────────────────────────────────────────────────────────────
|
|
166
|
+
function markdownFiles(dir, out = []) {
|
|
167
|
+
for (const e of readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
168
|
+
if (e.name === 'node_modules' || e.name.startsWith('.git')) continue;
|
|
169
|
+
const p = join(dir, e.name);
|
|
170
|
+
if (e.isDirectory()) markdownFiles(p, out);
|
|
171
|
+
else if (/\.(md|markdown)$/i.test(e.name)) out.push(p);
|
|
172
|
+
}
|
|
173
|
+
return out;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Claude Code's auto-memory for a project lives under a slug of its absolute path. Derived from
|
|
177
|
+
// HOME and the repo path — never a hardcoded user, because this file ships in a public package.
|
|
178
|
+
export function memoryDir(root, home = homedir()) {
|
|
179
|
+
return join(home, '.claude', 'projects', String(root).replace(/\//g, '-'), 'memory');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Scan one tree. Returns findings plus the number of claims actually examined — a check that
|
|
183
|
+
// silently stops looking would otherwise report "all clear" forever (see the count assertion in
|
|
184
|
+
// tests/drift-check.test.js).
|
|
185
|
+
export function scanTree(dir, { mode, root = REPO, label = null } = {}) {
|
|
186
|
+
const findings = [];
|
|
187
|
+
let claims = 0;
|
|
188
|
+
let skipped = 0;
|
|
189
|
+
for (const doc of markdownFiles(dir)) {
|
|
190
|
+
const text = readFileSync(doc, 'utf8');
|
|
191
|
+
const raw = mode === 'catalog'
|
|
192
|
+
? extractProseLinks(text)
|
|
193
|
+
: [...extractProseLinks(text), ...extractInlinePaths(text)];
|
|
194
|
+
const bases = basesFor(doc, { mode, root });
|
|
195
|
+
for (const c of raw) {
|
|
196
|
+
const cls = classify(c.target, { mode });
|
|
197
|
+
if (cls !== 'real') {
|
|
198
|
+
skipped++;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
claims++;
|
|
202
|
+
if (mode === 'knowledge' && resolvesInAnySkill(c.target, root)) continue;
|
|
203
|
+
if (!resolves(c.target, bases)) {
|
|
204
|
+
findings.push({
|
|
205
|
+
doc: relative(root, doc),
|
|
206
|
+
line: c.line,
|
|
207
|
+
target: c.target,
|
|
208
|
+
kind: c.kind,
|
|
209
|
+
tree: label || basename(dir),
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return { findings, claims, skipped };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ── the two modes ─────────────────────────────────────────────────────────────────────────
|
|
218
|
+
export class MissingRoot extends Error {}
|
|
219
|
+
|
|
220
|
+
// Blocking. A broken link in the catalog is broken product: the user follows a pointer to
|
|
221
|
+
// nowhere and cannot tell that from having misunderstood the skill.
|
|
222
|
+
export function checkCatalog({ root = REPO } = {}) {
|
|
223
|
+
const dir = join(root, 'skills');
|
|
224
|
+
if (!existsSync(dir)) throw new MissingRoot(`no catalog to check: ${relative(root, dir) || dir} does not exist`);
|
|
225
|
+
return scanTree(dir, { mode: 'catalog', root, label: 'skills' });
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Report-only. Inputs are the harness's own artifacts — written by a trusted agent, and for the
|
|
229
|
+
// wiki already reviewed. Session transcripts are NOT an input here and must not become one: they
|
|
230
|
+
// carry web pages, third-party READMEs and tool output, and anything derived from them would be
|
|
231
|
+
// loaded into every later session.
|
|
232
|
+
export function checkKnowledge({ root = REPO, home = homedir() } = {}) {
|
|
233
|
+
const trees = [
|
|
234
|
+
{ dir: join(root, '02-DOCS', 'wiki'), label: 'wiki' },
|
|
235
|
+
{ dir: memoryDir(root, home), label: 'memory' },
|
|
236
|
+
].filter((t) => existsSync(t.dir));
|
|
237
|
+
if (!trees.length) throw new MissingRoot('no knowledge tree found (02-DOCS/wiki and auto-memory are both absent)');
|
|
238
|
+
const all = { findings: [], claims: 0, skipped: 0, trees: trees.map((t) => t.label) };
|
|
239
|
+
for (const t of trees) {
|
|
240
|
+
const r = scanTree(t.dir, { mode: 'knowledge', root, label: t.label });
|
|
241
|
+
all.findings.push(...r.findings);
|
|
242
|
+
all.claims += r.claims;
|
|
243
|
+
all.skipped += r.skipped;
|
|
244
|
+
}
|
|
245
|
+
return all;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ── CLI ───────────────────────────────────────────────────────────────────────────────────
|
|
249
|
+
// Every denial carries its own exit (P6). The second half of the recovery matters as much as the
|
|
250
|
+
// first: an example link is fixed by putting it in code, and that is not a loophole — it is the
|
|
251
|
+
// rule this check is built on, stated where someone hitting the wall will read it.
|
|
252
|
+
const RECOVER = [
|
|
253
|
+
'Recover: fix the link so it resolves from the document that contains it,',
|
|
254
|
+
' or — if it is showing what a link looks like rather than pointing at a file —',
|
|
255
|
+
' wrap it in backticks or a fenced block. Code shows; prose points.',
|
|
256
|
+
].join('\n');
|
|
257
|
+
|
|
258
|
+
const report = (f) => ` ${f.doc}:${f.line} → ${f.target}${f.kind === 'inline' ? ' [inline]' : ''}`;
|
|
259
|
+
|
|
260
|
+
function main(argv) {
|
|
261
|
+
const wantKnowledge = argv.includes('--knowledge') || argv.includes('--all');
|
|
262
|
+
const wantCatalog = !argv.includes('--knowledge') || argv.includes('--all');
|
|
263
|
+
let failed = false;
|
|
264
|
+
|
|
265
|
+
if (wantCatalog) {
|
|
266
|
+
let r;
|
|
267
|
+
try {
|
|
268
|
+
r = checkCatalog({});
|
|
269
|
+
} catch (e) {
|
|
270
|
+
// Distinct exit and message: "could not run" must never be reported as "nothing wrong".
|
|
271
|
+
console.error(`drift-check: ${e.message}`);
|
|
272
|
+
console.error('Recover: run this from the repo root, where skills/ lives.');
|
|
273
|
+
process.exit(2);
|
|
274
|
+
}
|
|
275
|
+
console.log(`drift-check catalog: ${r.claims} prose link claim(s) checked, ${r.skipped} classified as non-claims`);
|
|
276
|
+
if (r.findings.length) {
|
|
277
|
+
console.log(`\nFAIL: ${r.findings.length} link(s) do not resolve:\n${r.findings.map(report).join('\n')}\n`);
|
|
278
|
+
console.log(RECOVER);
|
|
279
|
+
failed = true;
|
|
280
|
+
} else {
|
|
281
|
+
console.log('RESULT: PASS — every prose link in the catalog resolves');
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (wantKnowledge) {
|
|
286
|
+
try {
|
|
287
|
+
const k = checkKnowledge({});
|
|
288
|
+
console.log(`\ndrift-check knowledge (${k.trees.join(', ')}): ${k.claims} claim(s) checked, ${k.skipped} classified as non-claims`);
|
|
289
|
+
if (k.findings.length) {
|
|
290
|
+
console.log(`${k.findings.length} claim(s) do not resolve — advisory, nothing is blocked:\n${k.findings.map(report).join('\n')}`);
|
|
291
|
+
console.log('\nEach is either drift (fix the document) or a deliberate mention of something absent (leave it).');
|
|
292
|
+
} else {
|
|
293
|
+
console.log('no drift found');
|
|
294
|
+
}
|
|
295
|
+
} catch (e) {
|
|
296
|
+
// Advisory tree missing is normal (02-DOCS is gitignored; a fresh install has no wiki).
|
|
297
|
+
console.log(`\ndrift-check knowledge: skipped — ${e.message}`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
process.exit(failed ? 1 : 0);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Compared through realpath, not by string: on macOS the temp dir is a symlink, so argv[1] and
|
|
305
|
+
// import.meta.url disagree for any script under it and the common
|
|
306
|
+
// `import.meta.url === \`file://${process.argv[1]}\`` guard silently declines to run —
|
|
307
|
+
// which looks exactly like a clean pass.
|
|
308
|
+
function invokedDirectly() {
|
|
309
|
+
try {
|
|
310
|
+
return !!process.argv[1] && realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
|
|
311
|
+
} catch {
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (invokedDirectly()) main(process.argv.slice(2));
|
|
@@ -7,7 +7,7 @@ The three guards below — **diff judge**, **hold-out**, **Generalization gate**
|
|
|
7
7
|
mechanised: `tests/harden-generalization-gate.test.js` asserts, for each of those three, that the
|
|
8
8
|
loop implements what this file promises. **Adding a fourth guard here means adding its pair to that
|
|
9
9
|
test**; a guard documented but unchecked gives the safety without the protection, which is the
|
|
10
|
-
failure mode this repo has now hit
|
|
10
|
+
failure mode this repo has now hit ten times (constitution P2).
|
|
11
11
|
|
|
12
12
|
## Diagnosis (skill-fault vs eval-fault vs capability)
|
|
13
13
|
|
|
@@ -133,4 +133,4 @@ clean = clean.with_columns(
|
|
|
133
133
|
|
|
134
134
|
pandera validates Polars frames too (`import pandera.polars as pa`), so the gate from
|
|
135
135
|
[validation-patterns.md](validation-patterns.md) carries over. Hand multi-GB analytical SQL to
|
|
136
|
-
[`duckdb`](
|
|
136
|
+
[`duckdb`](../../duckdb/SKILL.md) — the engine changes, the discipline does not.
|
|
@@ -5,6 +5,9 @@ Append-only log of wanted-but-missing topics. Entries are flagged
|
|
|
5
5
|
deleted. Compaction (Deep Improve only) removes `[FILLED]` entries older
|
|
6
6
|
than 90 days, with a single compaction marker entry in `wiki/log.md`.
|
|
7
7
|
|
|
8
|
+
One entry per gap, in this shape:
|
|
9
|
+
|
|
10
|
+
```markdown
|
|
8
11
|
## [YYYY-MM-DD] gap | {concept}
|
|
9
12
|
|
|
10
13
|
Source: {where this gap was detected — a query that couldn't answer, N
|
|
@@ -16,3 +19,4 @@ Mentioned in: [Article A](topic/a.md); [Article B](topic/b.md)
|
|
|
16
19
|
Suggested topic: {topic where the new article would belong}
|
|
17
20
|
|
|
18
21
|
Status: open
|
|
22
|
+
```
|
|
@@ -87,7 +87,7 @@ export default function Todos() {
|
|
|
87
87
|
|
|
88
88
|
- **Deployment**: SolidStart uses Nitro-style adapters; for Vercel/Netlify/Node
|
|
89
89
|
targets configure the adapter in `app.config.ts`. Deployment specifics →
|
|
90
|
-
[
|
|
90
|
+
[`vercel`](../../vercel/SKILL.md).
|
|
91
91
|
|
|
92
92
|
Version note: SolidStart 1.x is the maintained stable line; **2.0.0-alpha** tracks
|
|
93
93
|
Solid 2.0. Default to 1.x unless the task targets the alpha.
|