instar 1.3.198 → 1.3.199
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/dist/core/ApprenticeshipProgram.d.ts +211 -0
- package/dist/core/ApprenticeshipProgram.d.ts.map +1 -0
- package/dist/core/ApprenticeshipProgram.js +433 -0
- package/dist/core/ApprenticeshipProgram.js.map +1 -0
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +19 -0
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/retroHarvestValidator.d.ts +79 -0
- package/dist/core/retroHarvestValidator.d.ts.map +1 -0
- package/dist/core/retroHarvestValidator.js +256 -0
- package/dist/core/retroHarvestValidator.js.map +1 -0
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +7 -0
- package/dist/scaffold/templates.js.map +1 -1
- package/dist/server/AgentServer.d.ts +1 -0
- package/dist/server/AgentServer.d.ts.map +1 -1
- package/dist/server/AgentServer.js +48 -0
- package/dist/server/AgentServer.js.map +1 -1
- package/dist/server/CapabilityIndex.d.ts.map +1 -1
- package/dist/server/CapabilityIndex.js +16 -0
- package/dist/server/CapabilityIndex.js.map +1 -1
- package/dist/server/routes.d.ts +5 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +99 -0
- package/dist/server/routes.js.map +1 -1
- package/package.json +1 -1
- package/scripts/validate-retro-harvest.mjs +31 -237
- package/src/data/builtin-manifest.json +63 -63
- package/src/scaffold/templates.ts +7 -0
- package/upgrades/1.3.199.md +24 -0
- package/upgrades/side-effects/APPRENTICESHIP-STEP1-PROGRAM-SCAFFOLD-SPEC.md +82 -0
package/package.json
CHANGED
|
@@ -1,19 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* validate-retro-harvest.mjs —
|
|
4
|
-
* retro-harvest
|
|
3
|
+
* validate-retro-harvest.mjs — thin CLI for the Apprenticeship Step 0
|
|
4
|
+
* retro-harvest validator.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* - validateRetroHarvest(text, opts) — PURE, offline, unit-tested. Structure,
|
|
13
|
-
* reconciliation, redaction backstop, path safety, completeness, scope rules.
|
|
14
|
-
* - checkLiveLedger(frontmatter, fetchImpl) — OPTIONAL live cross-check that
|
|
15
|
-
* seeded ids resolve in the running ledger. Network; run by the integration
|
|
16
|
-
* test / `--check-live`, never by the pure unit path.
|
|
6
|
+
* The PURE validator logic now lives in TypeScript at
|
|
7
|
+
* `src/core/retroHarvestValidator.ts` (the SOURCE OF TRUTH, Apprenticeship
|
|
8
|
+
* Step 1 §3.2 relocation). This script re-exports the compiled module so
|
|
9
|
+
* existing importers (tests via the TS source, callers via the build) keep
|
|
10
|
+
* working, and keeps the `node scripts/...` CLI entry. Precedent:
|
|
11
|
+
* `src/threadline/BackfillCore.ts` + its scripts consumer.
|
|
17
12
|
*
|
|
18
13
|
* CLI: node scripts/validate-retro-harvest.mjs <artifact.md> [--prior] [--check-live]
|
|
19
14
|
* exit 0 = valid, exit 1 = invalid (errors to stderr).
|
|
@@ -22,234 +17,33 @@
|
|
|
22
17
|
import { readFileSync } from 'node:fs';
|
|
23
18
|
import { fileURLToPath } from 'node:url';
|
|
24
19
|
import path from 'node:path';
|
|
25
|
-
import yaml from 'js-yaml';
|
|
26
|
-
|
|
27
|
-
export const SCHEMA_ID = 'apprenticeship-retro-harvest/v1';
|
|
28
|
-
export const INSTANCE_TYPES = ['mentorship', 'apprenticeship'];
|
|
29
|
-
export const SCOPE_MODES = ['full', 'incremental'];
|
|
30
|
-
export const COMPLETENESS = ['complete', 'partial-accepted'];
|
|
31
|
-
export const FIDELITY_VERDICTS = ['faithful', 'partial', 'rejected'];
|
|
32
|
-
export const HARVEST_DIR = 'docs/apprenticeship/retro-harvests';
|
|
33
|
-
|
|
34
|
-
/** Scrubbers Step 0 trusts. A missing/unknown/failed scrub refuses the artifact (§7). */
|
|
35
|
-
export const APPROVED_SCRUBBERS = ['correction-scrub', 'instar-pii-scrub'];
|
|
36
|
-
|
|
37
|
-
/** Evidence-pointer URI schemes (§6). */
|
|
38
|
-
export const POINTER_PATTERNS = {
|
|
39
|
-
ledger: /^ledger:[0-9a-f-]{6,}$/i, // validator-resolvable
|
|
40
|
-
pr: /^pr:\d+$/, // validator-resolvable
|
|
41
|
-
thread: /^thread:\d+#[\w.-]+$/, // immutable msg locator; fidelity-review-only
|
|
42
|
-
memory: /^memory:[a-z0-9_-]+@[0-9a-f]{6,}#[\w.-]+$/i, // slug@hash#anchor; fidelity-review-only
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
/** Secret-shaped strings the backstop rejects (§7). A LIMITED backstop, not a scrub. */
|
|
46
|
-
const SECRET_PATTERNS = [
|
|
47
|
-
{ name: 'bearer-token', re: /\bBearer\s+[A-Za-z0-9._-]{8,}/ },
|
|
48
|
-
{ name: 'tunnel-sig', re: /[?&]sig=[0-9a-f]{16,}/i },
|
|
49
|
-
{ name: 'long-hex-blob', re: /\b[0-9a-f]{40,}\b/i },
|
|
50
|
-
{ name: 'long-base64-blob', re: /\b[A-Za-z0-9+/]{50,}={0,2}\b/ },
|
|
51
|
-
{ name: 'email', re: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/ },
|
|
52
|
-
];
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Split a markdown doc into { frontmatter (parsed), body (string) }.
|
|
56
|
-
* Throws if there is no `---`-delimited YAML frontmatter.
|
|
57
|
-
*/
|
|
58
|
-
export function parseArtifact(text) {
|
|
59
|
-
const m = /^---\n([\s\S]*?)\n---\n?([\s\S]*)$/.exec(text);
|
|
60
|
-
if (!m) throw new Error('no YAML frontmatter (--- … ---) found');
|
|
61
|
-
const frontmatter = yaml.load(m[1]) || {};
|
|
62
|
-
return { frontmatter, body: m[2] || '' };
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/** Count top-level `- ` bullet items directly under a `## <heading>` section. */
|
|
66
|
-
export function countSectionItems(body, heading) {
|
|
67
|
-
const lines = body.split('\n');
|
|
68
|
-
let inSection = false;
|
|
69
|
-
let count = 0;
|
|
70
|
-
const headingRe = new RegExp('^##\\s+' + heading.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i');
|
|
71
|
-
for (const line of lines) {
|
|
72
|
-
if (/^##\s+/.test(line)) inSection = headingRe.test(line);
|
|
73
|
-
else if (inSection && /^-\s+\S/.test(line)) count++;
|
|
74
|
-
}
|
|
75
|
-
return count;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/** True when the body contains a secret-shaped string (returns the matched pattern name or null). */
|
|
79
|
-
export function findSecret(body) {
|
|
80
|
-
for (const { name, re } of SECRET_PATTERNS) {
|
|
81
|
-
if (re.test(body)) return name;
|
|
82
|
-
}
|
|
83
|
-
return null;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Resolve + confine an artifact path to HARVEST_DIR. Returns the safe relative
|
|
88
|
-
* path or throws on traversal / bad component (§6 security).
|
|
89
|
-
*/
|
|
90
|
-
export function safeArtifactPath(from, to, instanceType) {
|
|
91
|
-
for (const [k, v] of Object.entries({ from, to, instanceType })) {
|
|
92
|
-
if (typeof v !== 'string' || !/^[a-z0-9-]+$/.test(v)) {
|
|
93
|
-
throw new Error(`unsafe path component ${k}="${v}" (must match ^[a-z0-9-]+$)`);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
const rel = `${HARVEST_DIR}/${from}-to-${to}-${instanceType}.md`;
|
|
97
|
-
const resolved = path.normalize(rel);
|
|
98
|
-
if (resolved.includes('..') || !resolved.startsWith(HARVEST_DIR + path.sep) && !resolved.startsWith(HARVEST_DIR + '/')) {
|
|
99
|
-
throw new Error(`path escapes ${HARVEST_DIR}: ${resolved}`);
|
|
100
|
-
}
|
|
101
|
-
return rel;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* PURE structural validation of a retro-harvest artifact.
|
|
106
|
-
* @param {string} text - full artifact markdown
|
|
107
|
-
* @param {{ priorHarvestExists?: boolean }} [opts]
|
|
108
|
-
* @returns {{ valid: boolean, errors: string[] }}
|
|
109
|
-
*/
|
|
110
|
-
export function validateRetroHarvest(text, opts = {}) {
|
|
111
|
-
const errors = [];
|
|
112
|
-
const priorHarvestExists = !!opts.priorHarvestExists;
|
|
113
|
-
|
|
114
|
-
let parsed;
|
|
115
|
-
try {
|
|
116
|
-
parsed = parseArtifact(text);
|
|
117
|
-
} catch (e) {
|
|
118
|
-
return { valid: false, errors: [`parse: ${e.message}`] };
|
|
119
|
-
}
|
|
120
|
-
const { frontmatter: fm, body } = parsed;
|
|
121
20
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// --- scope rule: first harvest (no prior baseline) MUST be full (§8)
|
|
143
|
-
if (fm.scopeMode === 'incremental' && !priorHarvestExists)
|
|
144
|
-
errors.push('scopeMode "incremental" requires a prior harvest baseline; the first harvest must be "full"');
|
|
145
|
-
|
|
146
|
-
// --- sourcesCovered: coverage EXTENT, not bare booleans (§8)
|
|
147
|
-
const sc = fm.sourcesCovered;
|
|
148
|
-
if (!sc || typeof sc !== 'object') {
|
|
149
|
-
errors.push('missing sourcesCovered');
|
|
150
|
-
} else {
|
|
151
|
-
if (!sc.ledger || typeof sc.ledger.issueCount !== 'number')
|
|
152
|
-
errors.push('sourcesCovered.ledger must carry { read, issueCount }');
|
|
153
|
-
if (!sc.playbook || typeof sc.playbook.entryCount !== 'number')
|
|
154
|
-
errors.push('sourcesCovered.playbook must carry { read, entryCount }');
|
|
155
|
-
if (!Array.isArray(sc.threads))
|
|
156
|
-
errors.push('sourcesCovered.threads must be an array of { id, messagesRead, truncated }');
|
|
157
|
-
else for (const t of sc.threads) {
|
|
158
|
-
if (typeof t.messagesRead !== 'number' || typeof t.truncated !== 'boolean')
|
|
159
|
-
errors.push(`sourcesCovered.threads[${t && t.id}] needs numeric messagesRead + boolean truncated (coverage extent, not presence)`);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// --- completeness vs truncation (§8): complete ⇒ nothing truncated
|
|
164
|
-
if (fm.completeness === 'complete' && sc && Array.isArray(sc.threads)) {
|
|
165
|
-
if (sc.threads.some((t) => t.truncated === true))
|
|
166
|
-
errors.push('completeness "complete" is invalid when a source is truncated; use "partial-accepted" with named gaps');
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// --- counts reconcile with body (§9)
|
|
170
|
-
const counts = fm.counts || {};
|
|
171
|
-
const bodyLessons = countSectionItems(body, 'Lessons');
|
|
172
|
-
const bodyMeta = countSectionItems(body, 'Meta-lessons');
|
|
173
|
-
const bodyProc = countSectionItems(body, 'Process-insights');
|
|
174
|
-
const bodyNeeds = countSectionItems(body, 'What the program needs');
|
|
175
|
-
if (counts.lessons !== bodyLessons)
|
|
176
|
-
errors.push(`counts.lessons=${counts.lessons} but body has ${bodyLessons} "## Lessons" items`);
|
|
177
|
-
if (counts.metaLessons !== bodyMeta)
|
|
178
|
-
errors.push(`counts.metaLessons=${counts.metaLessons} but body has ${bodyMeta} "## Meta-lessons" items`);
|
|
179
|
-
if (counts.processInsights !== bodyProc)
|
|
180
|
-
errors.push(`counts.processInsights=${counts.processInsights} but body has ${bodyProc} "## Process-insights" items`);
|
|
181
|
-
if (fm.programNeeds !== bodyNeeds)
|
|
182
|
-
errors.push(`programNeeds=${fm.programNeeds} but body has ${bodyNeeds} "## What the program needs" items`);
|
|
183
|
-
|
|
184
|
-
// --- redaction: approved scrubber, not failed (§7)
|
|
185
|
-
const r = fm.redaction;
|
|
186
|
-
if (!r || typeof r !== 'object') {
|
|
187
|
-
errors.push('missing redaction { scrubber, findingsRemoved, scrubbedAt }');
|
|
188
|
-
} else {
|
|
189
|
-
const scrubberName = String(r.scrubber || '').split('@')[0];
|
|
190
|
-
if (!APPROVED_SCRUBBERS.includes(scrubberName))
|
|
191
|
-
errors.push(`redaction.scrubber "${r.scrubber}" not in approved list (${APPROVED_SCRUBBERS.join(', ')})`);
|
|
192
|
-
if (r.status === 'failed') errors.push('redaction.status is "failed" — scrub must succeed before write');
|
|
193
|
-
if (typeof r.findingsRemoved !== 'number') errors.push('redaction.findingsRemoved must be a number');
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// --- fidelity review: the authority's stamp (§9)
|
|
197
|
-
const fr = fm.fidelityReview;
|
|
198
|
-
if (!fr || typeof fr !== 'object') {
|
|
199
|
-
errors.push('missing fidelityReview { reviewer, verdict, at }');
|
|
200
|
-
} else {
|
|
201
|
-
if (!FIDELITY_VERDICTS.includes(fr.verdict))
|
|
202
|
-
errors.push(`fidelityReview.verdict must be one of ${FIDELITY_VERDICTS.join('|')}`);
|
|
203
|
-
if (fr.verdict === 'rejected') errors.push('fidelityReview.verdict is "rejected" — harvest is not faithful');
|
|
204
|
-
if (fr.verdict === 'partial' && !fr.gaps && !(fr.audit && fr.audit.gaps))
|
|
205
|
-
errors.push('fidelityReview "partial" must name the gaps (fidelityReview.gaps)');
|
|
206
|
-
if (!fr.reviewer) errors.push('fidelityReview.reviewer required (must be independent of the harvesting pass)');
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// --- evidence pointers well-formed (§6). Strip trailing sentence punctuation
|
|
210
|
-
// so a pointer at a clause boundary ("ledger:abc123, priority…") still validates.
|
|
211
|
-
const pointerTokens = (body.match(/\b(?:ledger|pr|thread|memory):[^\s)\]]+/g) || [])
|
|
212
|
-
.map((t) => t.replace(/[.,;:]+$/, ''));
|
|
213
|
-
for (const tok of pointerTokens) {
|
|
214
|
-
const scheme = tok.split(':')[0];
|
|
215
|
-
const re = POINTER_PATTERNS[scheme];
|
|
216
|
-
if (re && !re.test(tok)) errors.push(`malformed evidence pointer: ${tok}`);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// --- secret backstop (§7)
|
|
220
|
-
const secret = findSecret(body);
|
|
221
|
-
if (secret) errors.push(`secret-shaped string in body (${secret}) — pointers-not-payloads (§7)`);
|
|
222
|
-
|
|
223
|
-
return { valid: errors.length === 0, errors };
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* OPTIONAL live cross-check: every seededToPlaybook id resolves at candidate+ in
|
|
228
|
-
* the running ledger. Network — never part of the pure unit path. Inject fetch.
|
|
229
|
-
* @returns {Promise<{ ok: boolean, errors: string[] }>}
|
|
230
|
-
*/
|
|
231
|
-
export async function checkLiveLedger(frontmatter, fetchImpl) {
|
|
232
|
-
const errors = [];
|
|
233
|
-
const seeded = Array.isArray(frontmatter.seededToPlaybook) ? frontmatter.seededToPlaybook : [];
|
|
234
|
-
for (const s of seeded) {
|
|
235
|
-
const id = s && s.id;
|
|
236
|
-
if (!id) { errors.push('seededToPlaybook entry without id'); continue; }
|
|
237
|
-
try {
|
|
238
|
-
const res = await fetchImpl(id);
|
|
239
|
-
if (!res || !res.found) errors.push(`seeded id ${id} not found in live ledger`);
|
|
240
|
-
else if (!['candidate', 'extracted'].includes(res.playbookStatus))
|
|
241
|
-
errors.push(`seeded id ${id} is "${res.playbookStatus}", expected candidate+`);
|
|
242
|
-
} catch (e) {
|
|
243
|
-
errors.push(`live check for ${id} failed: ${e.message}`);
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
return { ok: errors.length === 0, errors };
|
|
247
|
-
}
|
|
21
|
+
export {
|
|
22
|
+
validateRetroHarvest,
|
|
23
|
+
parseArtifact,
|
|
24
|
+
countSectionItems,
|
|
25
|
+
findSecret,
|
|
26
|
+
safeArtifactPath,
|
|
27
|
+
checkLiveLedger,
|
|
28
|
+
SCHEMA_ID,
|
|
29
|
+
INSTANCE_TYPES,
|
|
30
|
+
SCOPE_MODES,
|
|
31
|
+
COMPLETENESS,
|
|
32
|
+
FIDELITY_VERDICTS,
|
|
33
|
+
HARVEST_DIR,
|
|
34
|
+
APPROVED_SCRUBBERS,
|
|
35
|
+
POINTER_PATTERNS,
|
|
36
|
+
} from '../dist/core/retroHarvestValidator.js';
|
|
37
|
+
|
|
38
|
+
import { validateRetroHarvest } from '../dist/core/retroHarvestValidator.js';
|
|
248
39
|
|
|
249
40
|
// --- CLI entry (guarded so importing for tests is side-effect-free)
|
|
250
41
|
const isMain = (() => {
|
|
251
|
-
try {
|
|
252
|
-
|
|
42
|
+
try {
|
|
43
|
+
return process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1]);
|
|
44
|
+
} catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
253
47
|
})();
|
|
254
48
|
|
|
255
49
|
if (isMain) {
|