@skill-map/cli 0.17.0 → 0.19.0
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/README.md +4 -0
- package/dist/cli/tutorial/sm-tutorial.md +7 -0
- package/dist/cli.js +12223 -6757
- package/dist/cli.js.map +1 -1
- package/dist/conformance/index.js +36 -14
- package/dist/conformance/index.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +776 -73
- package/dist/index.js.map +1 -1
- package/dist/kernel/index.d.ts +965 -134
- package/dist/kernel/index.js +776 -73
- package/dist/kernel/index.js.map +1 -1
- package/dist/migrations/001_initial.sql +122 -12
- package/dist/ui/chunk-3R7E3HPC.js +7 -0
- package/dist/ui/chunk-7CAK6MVK.js +2638 -0
- package/dist/ui/chunk-BORRASJB.js +247 -0
- package/dist/ui/chunk-CZSS4D6J.js +454 -0
- package/dist/ui/chunk-EQD7AYYJ.js +227 -0
- package/dist/ui/chunk-ETTRVTFV.js +1 -0
- package/dist/ui/chunk-LFIE4SCX.js +965 -0
- package/dist/ui/chunk-OKO3QOH6.js +1 -0
- package/dist/ui/chunk-PMIMYHBM.js +61 -0
- package/dist/ui/chunk-UHFGCO24.js +1 -0
- package/dist/ui/chunk-UJOZYR5I.js +1 -0
- package/dist/ui/chunk-VHIPW3TH.js +1 -0
- package/dist/ui/chunk-VWAUXWQX.js +237 -0
- package/dist/ui/index.html +10 -2
- package/dist/ui/main-BSYMJKTL.js +1 -0
- package/dist/ui/{styles-CBPFNGXA.css → styles-UAABA7VK.css} +1 -1
- package/migrations/001_initial.sql +122 -12
- package/package.json +6 -6
- package/dist/ui/chunk-5ZGVBIPP.js +0 -1031
- package/dist/ui/chunk-BWUDZKB6.js +0 -247
- package/dist/ui/chunk-LUDNWV6G.js +0 -3091
- package/dist/ui/chunk-WMWULWZX.js +0 -237
- package/dist/ui/main-7LR4JN4M.js +0 -1
package/dist/index.js
CHANGED
|
@@ -94,15 +94,16 @@ var Registry = class {
|
|
|
94
94
|
|
|
95
95
|
// kernel/orchestrator.ts
|
|
96
96
|
import { createHash } from "crypto";
|
|
97
|
-
import { existsSync as
|
|
97
|
+
import { existsSync as existsSync6, statSync as statSync2 } from "fs";
|
|
98
|
+
import { isAbsolute as isAbsolute2, resolve as resolvePath } from "path";
|
|
98
99
|
import { Tiktoken } from "js-tiktoken/lite";
|
|
99
100
|
import cl100k_base from "js-tiktoken/ranks/cl100k_base";
|
|
100
|
-
import
|
|
101
|
+
import yaml4 from "js-yaml";
|
|
101
102
|
|
|
102
103
|
// package.json
|
|
103
104
|
var package_default = {
|
|
104
105
|
name: "@skill-map/cli",
|
|
105
|
-
version: "0.
|
|
106
|
+
version: "0.19.0",
|
|
106
107
|
description: "skill-map reference implementation \u2014 kernel + CLI + adapters.",
|
|
107
108
|
license: "MIT",
|
|
108
109
|
type: "module",
|
|
@@ -160,15 +161,15 @@ var package_default = {
|
|
|
160
161
|
"pretest:ci": "tsup",
|
|
161
162
|
"pretest:coverage": "tsup",
|
|
162
163
|
"pretest:coverage:html": "tsup",
|
|
163
|
-
test: "tsc --noEmit && node --import tsx --test --test-reporter=spec 'test/**/*.test.ts' 'built-in-plugins/**/*.test.ts' 'kernel/**/*.test.ts'",
|
|
164
|
-
"test:ci": "tsc --noEmit && node --import tsx --test 'test/**/*.test.ts' 'built-in-plugins/**/*.test.ts' 'kernel/**/*.test.ts'",
|
|
165
|
-
"test:coverage": "tsc --noEmit && SKILL_MAP_SKIP_BENCHMARK=1 node --experimental-default-config-file --import tsx --test --experimental-test-coverage 'test/**/*.test.ts' 'built-in-plugins/**/*.test.ts' 'kernel/**/*.test.ts'",
|
|
166
|
-
"test:coverage:html": "tsc --noEmit && SKILL_MAP_SKIP_BENCHMARK=1 c8 node --import tsx --test 'test/**/*.test.ts' 'built-in-plugins/**/*.test.ts' 'kernel/**/*.test.ts'",
|
|
164
|
+
test: "tsc --noEmit && node --import tsx --test --test-reporter=spec 'test/**/*.test.ts' 'built-in-plugins/**/*.test.ts' 'kernel/**/*.test.ts' 'server/**/*.test.ts'",
|
|
165
|
+
"test:ci": "tsc --noEmit && node --import tsx --test 'test/**/*.test.ts' 'built-in-plugins/**/*.test.ts' 'kernel/**/*.test.ts' 'server/**/*.test.ts'",
|
|
166
|
+
"test:coverage": "tsc --noEmit && SKILL_MAP_SKIP_BENCHMARK=1 node --experimental-default-config-file --import tsx --test --experimental-test-coverage 'test/**/*.test.ts' 'built-in-plugins/**/*.test.ts' 'kernel/**/*.test.ts' 'server/**/*.test.ts'",
|
|
167
|
+
"test:coverage:html": "tsc --noEmit && SKILL_MAP_SKIP_BENCHMARK=1 c8 node --import tsx --test 'test/**/*.test.ts' 'built-in-plugins/**/*.test.ts' 'kernel/**/*.test.ts' 'server/**/*.test.ts'",
|
|
167
168
|
clean: "rm -rf dist coverage"
|
|
168
169
|
},
|
|
169
170
|
dependencies: {
|
|
170
171
|
"@hono/node-server": "2.0.1",
|
|
171
|
-
"@skill-map/spec": "0.
|
|
172
|
+
"@skill-map/spec": "0.19.0",
|
|
172
173
|
ajv: "8.18.0",
|
|
173
174
|
"ajv-formats": "3.0.1",
|
|
174
175
|
chokidar: "5.0.0",
|
|
@@ -205,6 +206,174 @@ var package_default = {
|
|
|
205
206
|
}
|
|
206
207
|
};
|
|
207
208
|
|
|
209
|
+
// kernel/sidecar/parse.ts
|
|
210
|
+
import { existsSync, readFileSync } from "fs";
|
|
211
|
+
import { dirname, resolve } from "path";
|
|
212
|
+
import { createRequire } from "module";
|
|
213
|
+
import { Ajv2020 } from "ajv/dist/2020.js";
|
|
214
|
+
import yaml from "js-yaml";
|
|
215
|
+
|
|
216
|
+
// kernel/util/ajv-interop.ts
|
|
217
|
+
import addFormatsModule from "ajv-formats";
|
|
218
|
+
var addFormats = addFormatsModule.default ?? addFormatsModule;
|
|
219
|
+
function applyAjvFormats(ajv) {
|
|
220
|
+
addFormats(ajv);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// kernel/sidecar/parse.ts
|
|
224
|
+
function readSidecarFor(mdAbsolutePath) {
|
|
225
|
+
const sidecarPath = sidecarPathFor(mdAbsolutePath);
|
|
226
|
+
if (!existsSync(sidecarPath)) {
|
|
227
|
+
return { parsed: null, present: false, issues: [] };
|
|
228
|
+
}
|
|
229
|
+
let raw;
|
|
230
|
+
try {
|
|
231
|
+
raw = readFileSync(sidecarPath, "utf8");
|
|
232
|
+
} catch (err) {
|
|
233
|
+
return {
|
|
234
|
+
parsed: null,
|
|
235
|
+
present: true,
|
|
236
|
+
issues: [{ message: `cannot read ${sidecarPath}: ${err.message}` }]
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
let parsedYaml;
|
|
240
|
+
try {
|
|
241
|
+
parsedYaml = yaml.load(raw);
|
|
242
|
+
} catch (err) {
|
|
243
|
+
return {
|
|
244
|
+
parsed: null,
|
|
245
|
+
present: true,
|
|
246
|
+
issues: [{ message: `malformed YAML in ${sidecarPath}: ${err.message}` }]
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
if (!isPlainObject(parsedYaml)) {
|
|
250
|
+
return {
|
|
251
|
+
parsed: null,
|
|
252
|
+
present: true,
|
|
253
|
+
issues: [{ message: `sidecar root must be a YAML mapping at ${sidecarPath}` }]
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
const sidecarValidator = getSidecarValidator();
|
|
257
|
+
if (!sidecarValidator(parsedYaml)) {
|
|
258
|
+
const errors = (sidecarValidator.errors ?? []).map((e) => `${e.instancePath || "(root)"} ${e.message ?? e.keyword}`).join("; ");
|
|
259
|
+
return {
|
|
260
|
+
parsed: null,
|
|
261
|
+
present: true,
|
|
262
|
+
issues: [{ message: `sidecar schema validation failed at ${sidecarPath}: ${errors}` }]
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
const root = parsedYaml;
|
|
266
|
+
const identityBlock = root["identity"];
|
|
267
|
+
const annotationsRaw = root["annotations"];
|
|
268
|
+
const annotations = isPlainObject(annotationsRaw) ? Object.keys(annotationsRaw).length === 0 ? null : annotationsRaw : null;
|
|
269
|
+
return {
|
|
270
|
+
parsed: {
|
|
271
|
+
filePath: sidecarPath,
|
|
272
|
+
identityBodyHash: String(identityBlock["bodyHash"]),
|
|
273
|
+
identityFrontmatterHash: String(identityBlock["frontmatterHash"]),
|
|
274
|
+
identityPath: String(identityBlock["path"]),
|
|
275
|
+
annotations,
|
|
276
|
+
raw: root
|
|
277
|
+
},
|
|
278
|
+
present: true,
|
|
279
|
+
issues: []
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
function sidecarPathFor(mdAbsolutePath) {
|
|
283
|
+
if (mdAbsolutePath.endsWith(".md")) {
|
|
284
|
+
return `${mdAbsolutePath.slice(0, -".md".length)}.sm`;
|
|
285
|
+
}
|
|
286
|
+
return `${mdAbsolutePath}.sm`;
|
|
287
|
+
}
|
|
288
|
+
function isPlainObject(value) {
|
|
289
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
290
|
+
}
|
|
291
|
+
var cachedSidecarValidator = null;
|
|
292
|
+
function getSidecarValidator() {
|
|
293
|
+
if (cachedSidecarValidator) return cachedSidecarValidator;
|
|
294
|
+
const ajv = new Ajv2020({ strict: false, allErrors: true, allowUnionTypes: true });
|
|
295
|
+
applyAjvFormats(ajv);
|
|
296
|
+
const specRoot = resolveSpecRoot();
|
|
297
|
+
const annotationsSchema = JSON.parse(
|
|
298
|
+
readFileSync(resolve(specRoot, "schemas/annotations.schema.json"), "utf8")
|
|
299
|
+
);
|
|
300
|
+
const sidecarSchema = JSON.parse(
|
|
301
|
+
readFileSync(resolve(specRoot, "schemas/sidecar.schema.json"), "utf8")
|
|
302
|
+
);
|
|
303
|
+
ajv.addSchema(annotationsSchema);
|
|
304
|
+
cachedSidecarValidator = ajv.compile(sidecarSchema);
|
|
305
|
+
return cachedSidecarValidator;
|
|
306
|
+
}
|
|
307
|
+
function resolveSpecRoot() {
|
|
308
|
+
const require2 = createRequire(import.meta.url);
|
|
309
|
+
try {
|
|
310
|
+
const indexPath = require2.resolve("@skill-map/spec/index.json");
|
|
311
|
+
return dirname(indexPath);
|
|
312
|
+
} catch {
|
|
313
|
+
throw new Error(
|
|
314
|
+
"@skill-map/spec not resolvable \u2014 sidecar reader cannot load schemas."
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// kernel/sidecar/drift.ts
|
|
320
|
+
function computeDriftStatus(args) {
|
|
321
|
+
const bodyDrift = args.storedBodyHash !== args.liveBodyHash;
|
|
322
|
+
const fmDrift = args.storedFrontmatterHash !== args.liveFrontmatterHash;
|
|
323
|
+
if (bodyDrift && fmDrift) return "stale-both";
|
|
324
|
+
if (bodyDrift) return "stale-body";
|
|
325
|
+
if (fmDrift) return "stale-frontmatter";
|
|
326
|
+
return "fresh";
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// kernel/sidecar/discover-orphans.ts
|
|
330
|
+
import { existsSync as existsSync2, readdirSync, statSync } from "fs";
|
|
331
|
+
import { join, relative, sep } from "path";
|
|
332
|
+
function discoverOrphanSidecars(roots, shouldSkip) {
|
|
333
|
+
const out = [];
|
|
334
|
+
for (const root of roots) {
|
|
335
|
+
walk(root, root, shouldSkip ?? (() => false), out);
|
|
336
|
+
}
|
|
337
|
+
return out;
|
|
338
|
+
}
|
|
339
|
+
function walk(root, current, shouldSkip, out) {
|
|
340
|
+
let entries;
|
|
341
|
+
try {
|
|
342
|
+
entries = readdirSync(current, { withFileTypes: true, encoding: "utf8" });
|
|
343
|
+
} catch {
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
for (const entry of entries) {
|
|
347
|
+
const full = join(current, entry.name);
|
|
348
|
+
const rel = relative(root, full).split(sep).join("/");
|
|
349
|
+
if (shouldSkip(rel)) continue;
|
|
350
|
+
if (entry.isSymbolicLink()) continue;
|
|
351
|
+
if (entry.isDirectory()) {
|
|
352
|
+
walk(root, full, shouldSkip, out);
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
if (!entry.isFile()) continue;
|
|
356
|
+
if (!entry.name.endsWith(".sm")) continue;
|
|
357
|
+
const expectedMd = `${full.slice(0, -".sm".length)}.md`;
|
|
358
|
+
if (existsSync2(expectedMd) && safeIsFile(expectedMd)) continue;
|
|
359
|
+
out.push({ sidecarPath: full, relativePath: rel, expectedMdPath: expectedMd });
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
function safeIsFile(path) {
|
|
363
|
+
try {
|
|
364
|
+
return statSync(path).isFile();
|
|
365
|
+
} catch {
|
|
366
|
+
return false;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// kernel/sidecar/store.ts
|
|
371
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2, renameSync, writeFileSync, unlinkSync } from "fs";
|
|
372
|
+
import { dirname as dirname2, resolve as resolve2 } from "path";
|
|
373
|
+
import { createRequire as createRequire2 } from "module";
|
|
374
|
+
import { Ajv2020 as Ajv20202 } from "ajv/dist/2020.js";
|
|
375
|
+
import yaml2 from "js-yaml";
|
|
376
|
+
|
|
208
377
|
// kernel/adapters/in-memory-progress.ts
|
|
209
378
|
var InMemoryProgressEmitter = class {
|
|
210
379
|
#listeners = /* @__PURE__ */ new Set();
|
|
@@ -253,20 +422,13 @@ function getActiveLogger() {
|
|
|
253
422
|
}
|
|
254
423
|
|
|
255
424
|
// kernel/adapters/plugin-loader.ts
|
|
256
|
-
import { createRequire } from "module";
|
|
257
|
-
import { existsSync, readFileSync, readdirSync } from "fs";
|
|
258
|
-
import { isAbsolute, join, relative, resolve } from "path";
|
|
425
|
+
import { createRequire as createRequire3 } from "module";
|
|
426
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3, readdirSync as readdirSync2 } from "fs";
|
|
427
|
+
import { isAbsolute, join as join2, relative as relative2, resolve as resolve3 } from "path";
|
|
259
428
|
import { pathToFileURL } from "url";
|
|
260
|
-
import { Ajv2020 } from "ajv/dist/2020.js";
|
|
429
|
+
import { Ajv2020 as Ajv20203 } from "ajv/dist/2020.js";
|
|
261
430
|
import semver from "semver";
|
|
262
431
|
|
|
263
|
-
// kernel/util/ajv-interop.ts
|
|
264
|
-
import addFormatsModule from "ajv-formats";
|
|
265
|
-
var addFormats = addFormatsModule.default ?? addFormatsModule;
|
|
266
|
-
function applyAjvFormats(ajv) {
|
|
267
|
-
addFormats(ajv);
|
|
268
|
-
}
|
|
269
|
-
|
|
270
432
|
// kernel/i18n/plugin-store.texts.ts
|
|
271
433
|
var PLUGIN_STORE_TEXTS = {
|
|
272
434
|
kvValidationFailed: "plugin '{{pluginId}}' ctx.store.set('{{key}}', value): value violates declared schema ({{schemaPath}}) \u2014 {{errors}}",
|
|
@@ -361,28 +523,140 @@ var KNOWN_KINDS = /* @__PURE__ */ new Set(["provider", "extractor", "rule", "act
|
|
|
361
523
|
var KNOWN_KINDS_LIST = [...KNOWN_KINDS].join(" / ");
|
|
362
524
|
var HOOKABLE_TRIGGERS_LIST = HOOK_TRIGGERS.join(", ");
|
|
363
525
|
function installedSpecVersion() {
|
|
364
|
-
const require2 =
|
|
526
|
+
const require2 = createRequire3(import.meta.url);
|
|
365
527
|
const indexPath = require2.resolve("@skill-map/spec/index.json");
|
|
366
|
-
const pkgPath =
|
|
367
|
-
const pkg = JSON.parse(
|
|
528
|
+
const pkgPath = resolve3(indexPath, "..", "package.json");
|
|
529
|
+
const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
|
|
368
530
|
return pkg.version;
|
|
369
531
|
}
|
|
370
532
|
|
|
371
533
|
// kernel/adapters/schema-validators.ts
|
|
372
|
-
import { readFileSync as
|
|
373
|
-
import { dirname, resolve as
|
|
374
|
-
import { createRequire as
|
|
375
|
-
import { Ajv2020 as
|
|
534
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
535
|
+
import { dirname as dirname3, resolve as resolve4 } from "path";
|
|
536
|
+
import { createRequire as createRequire4 } from "module";
|
|
537
|
+
import { Ajv2020 as Ajv20204 } from "ajv/dist/2020.js";
|
|
538
|
+
var SCHEMA_FILES = {
|
|
539
|
+
node: "schemas/node.schema.json",
|
|
540
|
+
link: "schemas/link.schema.json",
|
|
541
|
+
issue: "schemas/issue.schema.json",
|
|
542
|
+
"scan-result": "schemas/scan-result.schema.json",
|
|
543
|
+
"execution-record": "schemas/execution-record.schema.json",
|
|
544
|
+
"project-config": "schemas/project-config.schema.json",
|
|
545
|
+
"plugins-registry": "schemas/plugins-registry.schema.json",
|
|
546
|
+
job: "schemas/job.schema.json",
|
|
547
|
+
"report-base": "schemas/report-base.schema.json",
|
|
548
|
+
"conformance-case": "schemas/conformance-case.schema.json",
|
|
549
|
+
"history-stats": "schemas/history-stats.schema.json",
|
|
550
|
+
"extension-provider": "schemas/extensions/provider.schema.json",
|
|
551
|
+
"extension-extractor": "schemas/extensions/extractor.schema.json",
|
|
552
|
+
"extension-rule": "schemas/extensions/rule.schema.json",
|
|
553
|
+
"extension-action": "schemas/extensions/action.schema.json",
|
|
554
|
+
"extension-formatter": "schemas/extensions/formatter.schema.json",
|
|
555
|
+
"extension-hook": "schemas/extensions/hook.schema.json",
|
|
556
|
+
"frontmatter-base": "schemas/frontmatter/base.schema.json"
|
|
557
|
+
};
|
|
558
|
+
var SUPPORTING_SCHEMAS = [
|
|
559
|
+
"schemas/extensions/base.schema.json",
|
|
560
|
+
"schemas/frontmatter/base.schema.json",
|
|
561
|
+
"schemas/summaries/security-scanner.schema.json",
|
|
562
|
+
"schemas/view-contracts.schema.json",
|
|
563
|
+
"schemas/input-types.schema.json"
|
|
564
|
+
];
|
|
565
|
+
var cachedValidators = null;
|
|
566
|
+
function loadSchemaValidators() {
|
|
567
|
+
if (cachedValidators !== null) return cachedValidators;
|
|
568
|
+
cachedValidators = buildSchemaValidators();
|
|
569
|
+
return cachedValidators;
|
|
570
|
+
}
|
|
571
|
+
function buildSchemaValidators() {
|
|
572
|
+
const specRoot = resolveSpecRoot2();
|
|
573
|
+
const ajv = new Ajv20204({
|
|
574
|
+
strict: false,
|
|
575
|
+
allErrors: true,
|
|
576
|
+
allowUnionTypes: true
|
|
577
|
+
});
|
|
578
|
+
applyAjvFormats(ajv);
|
|
579
|
+
for (const rel of SUPPORTING_SCHEMAS) {
|
|
580
|
+
const file = resolve4(specRoot, rel);
|
|
581
|
+
if (!existsSyncSafe(file)) continue;
|
|
582
|
+
const schema = JSON.parse(readFileSync4(file, "utf8"));
|
|
583
|
+
ajv.addSchema(schema);
|
|
584
|
+
}
|
|
585
|
+
const validators = /* @__PURE__ */ new Map();
|
|
586
|
+
for (const [name, rel] of Object.entries(SCHEMA_FILES)) {
|
|
587
|
+
const file = resolve4(specRoot, rel);
|
|
588
|
+
const schema = JSON.parse(readFileSync4(file, "utf8"));
|
|
589
|
+
const byId = typeof schema.$id === "string" ? ajv.getSchema(schema.$id) : void 0;
|
|
590
|
+
validators.set(name, byId ?? ajv.compile(schema));
|
|
591
|
+
}
|
|
592
|
+
const extensionByKind = {
|
|
593
|
+
provider: "extension-provider",
|
|
594
|
+
extractor: "extension-extractor",
|
|
595
|
+
rule: "extension-rule",
|
|
596
|
+
action: "extension-action",
|
|
597
|
+
formatter: "extension-formatter",
|
|
598
|
+
hook: "extension-hook"
|
|
599
|
+
};
|
|
600
|
+
const pluginManifestValidator = ajv.compile({
|
|
601
|
+
$ref: "https://skill-map.dev/spec/v0/plugins-registry.schema.json#/$defs/PluginManifest"
|
|
602
|
+
});
|
|
603
|
+
const contributionValidators = /* @__PURE__ */ new Map();
|
|
604
|
+
const VIEW_CONTRACTS_ID = "https://skill-map.dev/spec/v0/view-contracts.schema.json";
|
|
605
|
+
function getContributionValidator(contract) {
|
|
606
|
+
const existing = contributionValidators.get(contract);
|
|
607
|
+
if (existing) return existing;
|
|
608
|
+
const ref = `${VIEW_CONTRACTS_ID}#/$defs/payloads/${contract}`;
|
|
609
|
+
let compiled;
|
|
610
|
+
try {
|
|
611
|
+
compiled = ajv.compile({ $ref: ref });
|
|
612
|
+
} catch {
|
|
613
|
+
return null;
|
|
614
|
+
}
|
|
615
|
+
contributionValidators.set(contract, compiled);
|
|
616
|
+
return compiled;
|
|
617
|
+
}
|
|
618
|
+
return {
|
|
619
|
+
getValidator(name) {
|
|
620
|
+
const v = validators.get(name);
|
|
621
|
+
if (!v) throw new Error(`Unknown schema: ${name}`);
|
|
622
|
+
return v;
|
|
623
|
+
},
|
|
624
|
+
validatorForExtension(kind) {
|
|
625
|
+
return validators.get(extensionByKind[kind]);
|
|
626
|
+
},
|
|
627
|
+
validate(name, data) {
|
|
628
|
+
const v = validators.get(name);
|
|
629
|
+
if (!v) throw new Error(`Unknown schema: ${name}`);
|
|
630
|
+
if (v(data)) return { ok: true, data };
|
|
631
|
+
const errors = (v.errors ?? []).map(formatError).join("; ");
|
|
632
|
+
return { ok: false, errors };
|
|
633
|
+
},
|
|
634
|
+
validatePluginManifest(data) {
|
|
635
|
+
if (pluginManifestValidator(data)) return { ok: true, data };
|
|
636
|
+
const errors = (pluginManifestValidator.errors ?? []).map(formatError).join("; ");
|
|
637
|
+
return { ok: false, errors };
|
|
638
|
+
},
|
|
639
|
+
validateContributionPayload(contract, payload) {
|
|
640
|
+
const validator = getContributionValidator(contract);
|
|
641
|
+
if (!validator) {
|
|
642
|
+
return { ok: false, errors: "unknown-contract" };
|
|
643
|
+
}
|
|
644
|
+
if (validator(payload)) return { ok: true };
|
|
645
|
+
const errors = (validator.errors ?? []).map(formatError).join("; ");
|
|
646
|
+
return { ok: false, errors };
|
|
647
|
+
}
|
|
648
|
+
};
|
|
649
|
+
}
|
|
376
650
|
function buildProviderFrontmatterValidator(providers) {
|
|
377
|
-
const specRoot =
|
|
378
|
-
const ajv = new
|
|
651
|
+
const specRoot = resolveSpecRoot2();
|
|
652
|
+
const ajv = new Ajv20204({
|
|
379
653
|
strict: false,
|
|
380
654
|
allErrors: true,
|
|
381
655
|
allowUnionTypes: true
|
|
382
656
|
});
|
|
383
657
|
applyAjvFormats(ajv);
|
|
384
|
-
const baseFile =
|
|
385
|
-
const baseSchema = JSON.parse(
|
|
658
|
+
const baseFile = resolve4(specRoot, "schemas/frontmatter/base.schema.json");
|
|
659
|
+
const baseSchema = JSON.parse(readFileSync4(baseFile, "utf8"));
|
|
386
660
|
ajv.addSchema(baseSchema);
|
|
387
661
|
registerProviderAuxiliarySchemas(ajv, providers);
|
|
388
662
|
const compiled = /* @__PURE__ */ new Map();
|
|
@@ -419,17 +693,25 @@ function registerProviderAuxiliarySchemas(ajv, providers) {
|
|
|
419
693
|
}
|
|
420
694
|
}
|
|
421
695
|
}
|
|
422
|
-
function
|
|
423
|
-
const require2 =
|
|
696
|
+
function resolveSpecRoot2() {
|
|
697
|
+
const require2 = createRequire4(import.meta.url);
|
|
424
698
|
try {
|
|
425
699
|
const indexPath = require2.resolve("@skill-map/spec/index.json");
|
|
426
|
-
return
|
|
700
|
+
return dirname3(indexPath);
|
|
427
701
|
} catch {
|
|
428
702
|
throw new Error(
|
|
429
703
|
"@skill-map/spec not resolvable \u2014 ensure the workspace is linked or the package is installed."
|
|
430
704
|
);
|
|
431
705
|
}
|
|
432
706
|
}
|
|
707
|
+
function existsSyncSafe(path) {
|
|
708
|
+
try {
|
|
709
|
+
readFileSync4(path, "utf8");
|
|
710
|
+
return true;
|
|
711
|
+
} catch {
|
|
712
|
+
return false;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
433
715
|
|
|
434
716
|
// kernel/i18n/orchestrator.texts.ts
|
|
435
717
|
var ORCHESTRATOR_TEXTS = {
|
|
@@ -439,10 +721,201 @@ var ORCHESTRATOR_TEXTS = {
|
|
|
439
721
|
frontmatterMalformedMissingClose: "Frontmatter in {{path}} opens with `---` but never closes \u2014 no matching `---` line at column 0 was found. The file was scanned as body-only and every metadata field was silently lost. Add a closing `---` line below the metadata block.",
|
|
440
722
|
extensionErrorLinkKindNotDeclared: 'Extractor "{{extractorId}}" emitted a link of kind "{{linkKind}}" outside its declared `emitsLinkKinds` set [{{declaredKinds}}]. Link dropped.',
|
|
441
723
|
extensionErrorIssueInvalidSeverity: `Rule "{{ruleId}}" emitted an issue with invalid severity {{severity}} (allowed: 'error' | 'warn' | 'info'). Issue dropped.`,
|
|
724
|
+
extensionErrorContributionUnknownId: 'Extractor "{{extractorId}}" emitted contribution "{{contributionId}}" on {{nodePath}} but did not declare it in its `viewContributions` map. Contribution dropped.',
|
|
725
|
+
extensionErrorContributionPayloadInvalid: 'Extractor "{{extractorId}}" emitted contribution "{{contributionId}}" on {{nodePath}}; payload failed the "{{contract}}" schema: {{errors}}. Contribution dropped.',
|
|
442
726
|
runScanRootEmptyArray: "runScan: roots must contain at least one path (spec requires minItems: 1)",
|
|
443
727
|
runScanRootMissing: "runScan: root path '{{root}}' does not exist or is not a directory"
|
|
444
728
|
};
|
|
445
729
|
|
|
730
|
+
// kernel/util/format-error.ts
|
|
731
|
+
function formatErrorMessage(err) {
|
|
732
|
+
return err instanceof Error ? err.message : String(err);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// kernel/scan/walk-content.ts
|
|
736
|
+
import { readFile, readdir, stat } from "fs/promises";
|
|
737
|
+
import { join as join3, relative as relative3, sep as sep2 } from "path";
|
|
738
|
+
|
|
739
|
+
// kernel/scan/ignore.ts
|
|
740
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
|
|
741
|
+
import { dirname as dirname4, resolve as resolve5 } from "path";
|
|
742
|
+
import { fileURLToPath } from "url";
|
|
743
|
+
import ignoreFactory from "ignore";
|
|
744
|
+
function buildIgnoreFilter(opts = {}) {
|
|
745
|
+
const ig = ignoreFactory();
|
|
746
|
+
if (opts.includeDefaults !== false) {
|
|
747
|
+
ig.add(loadDefaultsText());
|
|
748
|
+
}
|
|
749
|
+
if (opts.configIgnore && opts.configIgnore.length > 0) {
|
|
750
|
+
ig.add(opts.configIgnore);
|
|
751
|
+
}
|
|
752
|
+
if (opts.ignoreFileText && opts.ignoreFileText.length > 0) {
|
|
753
|
+
ig.add(opts.ignoreFileText);
|
|
754
|
+
}
|
|
755
|
+
return {
|
|
756
|
+
ignores(relativePath) {
|
|
757
|
+
if (relativePath === "" || relativePath === "." || relativePath === "./") {
|
|
758
|
+
return false;
|
|
759
|
+
}
|
|
760
|
+
const normalised = relativePath.replace(/^\.\//, "").replace(/\\/g, "/").replace(/^\//, "");
|
|
761
|
+
if (normalised === "") return false;
|
|
762
|
+
return ig.ignores(normalised);
|
|
763
|
+
}
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
var cachedDefaults = null;
|
|
767
|
+
function loadDefaultsText() {
|
|
768
|
+
if (cachedDefaults !== null) return cachedDefaults;
|
|
769
|
+
cachedDefaults = readDefaultsFromDisk();
|
|
770
|
+
return cachedDefaults;
|
|
771
|
+
}
|
|
772
|
+
function readDefaultsFromDisk() {
|
|
773
|
+
const here = dirname4(fileURLToPath(import.meta.url));
|
|
774
|
+
const candidates = [
|
|
775
|
+
resolve5(here, "../../config/defaults/skillmapignore"),
|
|
776
|
+
// src/kernel/scan/ → src/config/defaults/
|
|
777
|
+
resolve5(here, "../config/defaults/skillmapignore"),
|
|
778
|
+
// dist/cli.js → dist/config/defaults/ (siblings)
|
|
779
|
+
resolve5(here, "config/defaults/skillmapignore")
|
|
780
|
+
];
|
|
781
|
+
for (const candidate of candidates) {
|
|
782
|
+
if (existsSync5(candidate)) {
|
|
783
|
+
try {
|
|
784
|
+
return readFileSync5(candidate, "utf8");
|
|
785
|
+
} catch {
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
return "";
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// kernel/scan/parsers/frontmatter-yaml.ts
|
|
793
|
+
import yaml3 from "js-yaml";
|
|
794
|
+
var FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
|
|
795
|
+
var FORBIDDEN_FRONTMATTER_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
796
|
+
var frontmatterYamlParser = {
|
|
797
|
+
id: "frontmatter-yaml",
|
|
798
|
+
parse(raw, _path) {
|
|
799
|
+
const match = FRONTMATTER_RE.exec(raw);
|
|
800
|
+
if (!match) return { frontmatterRaw: "", frontmatter: {}, body: raw };
|
|
801
|
+
const frontmatterRaw = match[1];
|
|
802
|
+
const body = match[2];
|
|
803
|
+
const parsed = {};
|
|
804
|
+
try {
|
|
805
|
+
const doc = yaml3.load(frontmatterRaw, { schema: yaml3.JSON_SCHEMA });
|
|
806
|
+
if (doc && typeof doc === "object" && !Array.isArray(doc)) {
|
|
807
|
+
for (const [k, v] of Object.entries(doc)) {
|
|
808
|
+
if (FORBIDDEN_FRONTMATTER_KEYS.has(k)) continue;
|
|
809
|
+
parsed[k] = v;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
} catch {
|
|
813
|
+
}
|
|
814
|
+
return { frontmatterRaw, frontmatter: parsed, body };
|
|
815
|
+
}
|
|
816
|
+
};
|
|
817
|
+
|
|
818
|
+
// kernel/scan/parsers/plain.ts
|
|
819
|
+
var plainParser = {
|
|
820
|
+
id: "plain",
|
|
821
|
+
parse(raw, _path) {
|
|
822
|
+
return { frontmatter: {}, frontmatterRaw: "", body: raw };
|
|
823
|
+
}
|
|
824
|
+
};
|
|
825
|
+
|
|
826
|
+
// kernel/scan/parsers/index.ts
|
|
827
|
+
var REGISTRY = /* @__PURE__ */ new Map([
|
|
828
|
+
[frontmatterYamlParser.id, frontmatterYamlParser],
|
|
829
|
+
[plainParser.id, plainParser]
|
|
830
|
+
]);
|
|
831
|
+
var FROZEN_IDS = new Set(REGISTRY.keys());
|
|
832
|
+
function getParser(id) {
|
|
833
|
+
return REGISTRY.get(id);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// kernel/scan/walk-content.ts
|
|
837
|
+
var UnknownParserError = class extends Error {
|
|
838
|
+
constructor(parserId) {
|
|
839
|
+
super(`Unknown parser id '${parserId}'. Built-in parsers: 'frontmatter-yaml', 'plain'.`);
|
|
840
|
+
this.name = "UnknownParserError";
|
|
841
|
+
}
|
|
842
|
+
};
|
|
843
|
+
async function* walkContent(roots, options) {
|
|
844
|
+
const parser = getParser(options.parser);
|
|
845
|
+
if (!parser) throw new UnknownParserError(options.parser);
|
|
846
|
+
const filter = options.ignoreFilter ?? buildIgnoreFilter();
|
|
847
|
+
const extensions = options.extensions;
|
|
848
|
+
for (const root of roots) {
|
|
849
|
+
for await (const file of walkRoot(root, root, filter, extensions)) {
|
|
850
|
+
const relPath = relative3(root, file).split(sep2).join("/");
|
|
851
|
+
let raw;
|
|
852
|
+
try {
|
|
853
|
+
raw = await readFile(file, "utf8");
|
|
854
|
+
} catch {
|
|
855
|
+
continue;
|
|
856
|
+
}
|
|
857
|
+
const parsed = parser.parse(raw, relPath);
|
|
858
|
+
yield {
|
|
859
|
+
path: relPath,
|
|
860
|
+
body: parsed.body,
|
|
861
|
+
frontmatterRaw: parsed.frontmatterRaw,
|
|
862
|
+
frontmatter: parsed.frontmatter
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
async function* walkRoot(root, current, filter, extensions) {
|
|
868
|
+
let entries;
|
|
869
|
+
try {
|
|
870
|
+
entries = await readdir(current, { withFileTypes: true, encoding: "utf8" });
|
|
871
|
+
} catch {
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
for (const entry of entries) {
|
|
875
|
+
const name = entry.name;
|
|
876
|
+
const full = join3(current, name);
|
|
877
|
+
const rel = relative3(root, full).split(sep2).join("/");
|
|
878
|
+
if (filter.ignores(rel)) continue;
|
|
879
|
+
if (entry.isSymbolicLink()) continue;
|
|
880
|
+
if (entry.isDirectory()) {
|
|
881
|
+
yield* walkRoot(root, full, filter, extensions);
|
|
882
|
+
} else if (entry.isFile() && hasMatchingExtension(name, extensions)) {
|
|
883
|
+
try {
|
|
884
|
+
const s = await stat(full);
|
|
885
|
+
if (s.isFile()) yield full;
|
|
886
|
+
} catch {
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
function hasMatchingExtension(name, extensions) {
|
|
892
|
+
for (const ext of extensions) {
|
|
893
|
+
if (name.endsWith(ext)) return true;
|
|
894
|
+
}
|
|
895
|
+
return false;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// kernel/extensions/provider.ts
|
|
899
|
+
var DEFAULT_READ_CONFIG = Object.freeze({
|
|
900
|
+
extensions: Object.freeze([".md"]),
|
|
901
|
+
parser: "frontmatter-yaml"
|
|
902
|
+
});
|
|
903
|
+
function resolveProviderWalk(provider) {
|
|
904
|
+
if (provider.walk) {
|
|
905
|
+
const walk2 = provider.walk.bind(provider);
|
|
906
|
+
return walk2;
|
|
907
|
+
}
|
|
908
|
+
const read = provider.read ?? DEFAULT_READ_CONFIG;
|
|
909
|
+
return (roots, options) => {
|
|
910
|
+
const walkOptions = {
|
|
911
|
+
extensions: read.extensions,
|
|
912
|
+
parser: read.parser
|
|
913
|
+
};
|
|
914
|
+
if (options?.ignoreFilter) walkOptions.ignoreFilter = options.ignoreFilter;
|
|
915
|
+
return walkContent(roots, walkOptions);
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
|
|
446
919
|
// kernel/orchestrator.ts
|
|
447
920
|
var SCANNED_BY = {
|
|
448
921
|
name: "skill-map",
|
|
@@ -505,14 +978,26 @@ async function runScanInternal(_kernel, options) {
|
|
|
505
978
|
emitter.emit(evt);
|
|
506
979
|
await hookDispatcher.dispatch("extractor.completed", evt);
|
|
507
980
|
}
|
|
508
|
-
const
|
|
981
|
+
const ruleResult = await runRules(
|
|
982
|
+
exts.rules,
|
|
983
|
+
walked.nodes,
|
|
984
|
+
walked.internalLinks,
|
|
985
|
+
walked.orphanSidecars,
|
|
986
|
+
walked.sidecarRoots,
|
|
987
|
+
options.annotationContributions ?? [],
|
|
988
|
+
options.viewContributions ?? [],
|
|
989
|
+
emitter,
|
|
990
|
+
hookDispatcher
|
|
991
|
+
);
|
|
992
|
+
const issues = ruleResult.issues;
|
|
993
|
+
for (const c of ruleResult.contributions) walked.contributions.push(c);
|
|
509
994
|
for (const issue of walked.frontmatterIssues) issues.push(issue);
|
|
510
995
|
const renameOps = prior ? detectRenamesAndOrphans(prior, walked.nodes, issues) : [];
|
|
511
996
|
const stats = {
|
|
512
997
|
// `filesSkipped` is "files walked but not classified by any Provider".
|
|
513
998
|
// Today every walked file IS classified by its Provider (the `claude`
|
|
514
999
|
// Provider's `classify()` always returns a kind, falling back to
|
|
515
|
-
// `'
|
|
1000
|
+
// `'markdown'`), so this is always 0. Wired now so the field shape is
|
|
516
1001
|
// spec-conformant; meaningful once multiple Providers compete.
|
|
517
1002
|
filesWalked: walked.filesWalked,
|
|
518
1003
|
filesSkipped: 0,
|
|
@@ -539,7 +1024,8 @@ async function runScanInternal(_kernel, options) {
|
|
|
539
1024
|
},
|
|
540
1025
|
renameOps,
|
|
541
1026
|
extractorRuns: walked.extractorRuns,
|
|
542
|
-
enrichments: walked.enrichments
|
|
1027
|
+
enrichments: walked.enrichments,
|
|
1028
|
+
contributions: walked.contributions
|
|
543
1029
|
};
|
|
544
1030
|
}
|
|
545
1031
|
function validateRoots(roots) {
|
|
@@ -547,7 +1033,7 @@ function validateRoots(roots) {
|
|
|
547
1033
|
throw new Error(ORCHESTRATOR_TEXTS.runScanRootEmptyArray);
|
|
548
1034
|
}
|
|
549
1035
|
for (const root of roots) {
|
|
550
|
-
if (!
|
|
1036
|
+
if (!existsSync6(root) || !statSync2(root).isDirectory()) {
|
|
551
1037
|
throw new Error(tx(ORCHESTRATOR_TEXTS.runScanRootMissing, { root }));
|
|
552
1038
|
}
|
|
553
1039
|
}
|
|
@@ -584,9 +1070,10 @@ async function runExtractorsForNode(opts) {
|
|
|
584
1070
|
const internalLinks = [];
|
|
585
1071
|
const externalLinks = [];
|
|
586
1072
|
const enrichmentBuffer = /* @__PURE__ */ new Map();
|
|
1073
|
+
const contributions = [];
|
|
1074
|
+
const validators = loadSchemaValidators();
|
|
587
1075
|
for (const extractor of opts.extractors) {
|
|
588
1076
|
const qualifiedId = qualifiedExtensionId(extractor.pluginId, extractor.id);
|
|
589
|
-
const isProb = extractor.mode === "probabilistic";
|
|
590
1077
|
const emitLink = (link) => {
|
|
591
1078
|
const validated = validateLink(extractor, link, opts.emitter);
|
|
592
1079
|
if (!validated) return;
|
|
@@ -606,9 +1093,54 @@ async function runExtractorsForNode(opts) {
|
|
|
606
1093
|
bodyHashAtEnrichment: opts.bodyHash,
|
|
607
1094
|
value: { ...partial },
|
|
608
1095
|
enrichedAt: Date.now(),
|
|
609
|
-
|
|
1096
|
+
// Extractors are deterministic-only; `is_probabilistic` is
|
|
1097
|
+
// reserved on the row for future Action-issued enrichments.
|
|
1098
|
+
isProbabilistic: false
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
1101
|
+
};
|
|
1102
|
+
const declaredContributions = readDeclaredContributions(extractor);
|
|
1103
|
+
const emitContribution = (contributionId, payload) => {
|
|
1104
|
+
const declared = declaredContributions.get(contributionId);
|
|
1105
|
+
if (!declared) {
|
|
1106
|
+
emitExtensionError(opts.emitter, qualifiedId, opts.node.path, {
|
|
1107
|
+
phase: "emitContribution",
|
|
1108
|
+
contributionId,
|
|
1109
|
+
reason: "unknown-contribution-id",
|
|
1110
|
+
message: tx(ORCHESTRATOR_TEXTS.extensionErrorContributionUnknownId, {
|
|
1111
|
+
extractorId: qualifiedId,
|
|
1112
|
+
contributionId,
|
|
1113
|
+
nodePath: opts.node.path
|
|
1114
|
+
})
|
|
1115
|
+
});
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1118
|
+
const result = validators.validateContributionPayload(declared.contract, payload);
|
|
1119
|
+
if (!result.ok) {
|
|
1120
|
+
emitExtensionError(opts.emitter, qualifiedId, opts.node.path, {
|
|
1121
|
+
phase: "emitContribution",
|
|
1122
|
+
contributionId,
|
|
1123
|
+
contract: declared.contract,
|
|
1124
|
+
reason: result.errors,
|
|
1125
|
+
message: tx(ORCHESTRATOR_TEXTS.extensionErrorContributionPayloadInvalid, {
|
|
1126
|
+
extractorId: qualifiedId,
|
|
1127
|
+
contributionId,
|
|
1128
|
+
nodePath: opts.node.path,
|
|
1129
|
+
contract: declared.contract,
|
|
1130
|
+
errors: result.errors
|
|
1131
|
+
})
|
|
610
1132
|
});
|
|
1133
|
+
return;
|
|
611
1134
|
}
|
|
1135
|
+
contributions.push({
|
|
1136
|
+
pluginId: extractor.pluginId,
|
|
1137
|
+
extensionId: extractor.id,
|
|
1138
|
+
nodePath: opts.node.path,
|
|
1139
|
+
contributionId,
|
|
1140
|
+
contract: declared.contract,
|
|
1141
|
+
payload,
|
|
1142
|
+
emittedAt: Date.now()
|
|
1143
|
+
});
|
|
612
1144
|
};
|
|
613
1145
|
const store = opts.pluginStores?.get(extractor.pluginId);
|
|
614
1146
|
const ctx = buildExtractorContext(
|
|
@@ -618,6 +1150,7 @@ async function runExtractorsForNode(opts) {
|
|
|
618
1150
|
opts.frontmatter,
|
|
619
1151
|
emitLink,
|
|
620
1152
|
enrichNode,
|
|
1153
|
+
emitContribution,
|
|
621
1154
|
store
|
|
622
1155
|
);
|
|
623
1156
|
await extractor.extract(ctx);
|
|
@@ -625,9 +1158,32 @@ async function runExtractorsForNode(opts) {
|
|
|
625
1158
|
return {
|
|
626
1159
|
internalLinks,
|
|
627
1160
|
externalLinks,
|
|
628
|
-
enrichments: Array.from(enrichmentBuffer.values())
|
|
1161
|
+
enrichments: Array.from(enrichmentBuffer.values()),
|
|
1162
|
+
contributions
|
|
629
1163
|
};
|
|
630
1164
|
}
|
|
1165
|
+
function readDeclaredContributions(extension) {
|
|
1166
|
+
const out = /* @__PURE__ */ new Map();
|
|
1167
|
+
const raw = extension.viewContributions;
|
|
1168
|
+
if (typeof raw !== "object" || raw === null) return out;
|
|
1169
|
+
for (const [id, value] of Object.entries(raw)) {
|
|
1170
|
+
if (typeof value !== "object" || value === null) continue;
|
|
1171
|
+
const contract = value.contract;
|
|
1172
|
+
if (typeof contract !== "string") continue;
|
|
1173
|
+
out.set(id, { contract });
|
|
1174
|
+
}
|
|
1175
|
+
return out;
|
|
1176
|
+
}
|
|
1177
|
+
function emitExtensionError(emitter, qualifiedId, nodePath, data) {
|
|
1178
|
+
emitter.emit(
|
|
1179
|
+
makeEvent("extension.error", {
|
|
1180
|
+
kind: "contribution-rejected",
|
|
1181
|
+
extensionId: qualifiedId,
|
|
1182
|
+
nodePath,
|
|
1183
|
+
...data
|
|
1184
|
+
})
|
|
1185
|
+
);
|
|
1186
|
+
}
|
|
631
1187
|
function computeCacheDecision(opts) {
|
|
632
1188
|
const applicableExtractors = opts.extractors.filter(
|
|
633
1189
|
(ex) => ex.applicableKinds === void 0 || ex.applicableKinds.includes(opts.kind)
|
|
@@ -750,7 +1306,9 @@ async function walkAndExtract(opts) {
|
|
|
750
1306
|
const cachedPaths = /* @__PURE__ */ new Set();
|
|
751
1307
|
const frontmatterIssues = [];
|
|
752
1308
|
const enrichmentBuffer = /* @__PURE__ */ new Map();
|
|
1309
|
+
const contributionsBuffer = [];
|
|
753
1310
|
const extractorRuns = [];
|
|
1311
|
+
const sidecarRoots = /* @__PURE__ */ new Map();
|
|
754
1312
|
let filesWalked = 0;
|
|
755
1313
|
let index = 0;
|
|
756
1314
|
const walkOptions = ignoreFilter ? { ignoreFilter } : {};
|
|
@@ -761,14 +1319,20 @@ async function walkAndExtract(opts) {
|
|
|
761
1319
|
if (list) list.push(qualified);
|
|
762
1320
|
else shortIdToQualified.set(ex.id, [qualified]);
|
|
763
1321
|
}
|
|
1322
|
+
const claimedPaths = /* @__PURE__ */ new Set();
|
|
764
1323
|
for (const provider of providers) {
|
|
765
|
-
for await (const raw of provider
|
|
1324
|
+
for await (const raw of resolveProviderWalk(provider)(roots, walkOptions)) {
|
|
766
1325
|
filesWalked += 1;
|
|
1326
|
+
if (claimedPaths.has(raw.path)) continue;
|
|
767
1327
|
const bodyHash = sha256(raw.body);
|
|
768
1328
|
const frontmatterHash = sha256(canonicalFrontmatter(raw.frontmatter, raw.frontmatterRaw));
|
|
769
1329
|
const priorNode = priorNodesByPath.get(raw.path);
|
|
770
1330
|
const nodeHashCacheEligible = enableCache && prior !== null && priorNode !== void 0 && priorNode.bodyHash === bodyHash && priorNode.frontmatterHash === frontmatterHash;
|
|
771
1331
|
const kind = provider.classify(raw.path, raw.frontmatter);
|
|
1332
|
+
if (kind === null) {
|
|
1333
|
+
continue;
|
|
1334
|
+
}
|
|
1335
|
+
claimedPaths.add(raw.path);
|
|
772
1336
|
index += 1;
|
|
773
1337
|
const cacheDecision = computeCacheDecision({
|
|
774
1338
|
extractors,
|
|
@@ -796,10 +1360,19 @@ async function walkAndExtract(opts) {
|
|
|
796
1360
|
priorLinksByOriginating,
|
|
797
1361
|
priorFrontmatterIssuesByNode
|
|
798
1362
|
});
|
|
1363
|
+
const reusedSidecarIssues = resolveAndApplySidecar(
|
|
1364
|
+
reused.node,
|
|
1365
|
+
raw.path,
|
|
1366
|
+
roots,
|
|
1367
|
+
bodyHash,
|
|
1368
|
+
frontmatterHash,
|
|
1369
|
+
sidecarRoots
|
|
1370
|
+
);
|
|
799
1371
|
nodes.push(reused.node);
|
|
800
1372
|
cachedPaths.add(reused.node.path);
|
|
801
1373
|
for (const link of reused.internalLinks) internalLinks.push(link);
|
|
802
1374
|
for (const issue of reused.frontmatterIssues) frontmatterIssues.push(issue);
|
|
1375
|
+
for (const issue of reusedSidecarIssues) frontmatterIssues.push(issue);
|
|
803
1376
|
for (const run of reused.extractorRuns) extractorRuns.push(run);
|
|
804
1377
|
emitter.emit(makeEvent("scan.progress", { index, path: raw.path, kind, cached: true }));
|
|
805
1378
|
continue;
|
|
@@ -835,6 +1408,15 @@ async function walkAndExtract(opts) {
|
|
|
835
1408
|
nodes.push(node);
|
|
836
1409
|
for (const issue of fresh.frontmatterIssues) frontmatterIssues.push(issue);
|
|
837
1410
|
}
|
|
1411
|
+
const sidecarIssues = resolveAndApplySidecar(
|
|
1412
|
+
node,
|
|
1413
|
+
raw.path,
|
|
1414
|
+
roots,
|
|
1415
|
+
bodyHash,
|
|
1416
|
+
frontmatterHash,
|
|
1417
|
+
sidecarRoots
|
|
1418
|
+
);
|
|
1419
|
+
for (const issue of sidecarIssues) frontmatterIssues.push(issue);
|
|
838
1420
|
emitter.emit(makeEvent("scan.progress", {
|
|
839
1421
|
index,
|
|
840
1422
|
path: raw.path,
|
|
@@ -857,6 +1439,7 @@ async function walkAndExtract(opts) {
|
|
|
857
1439
|
for (const enr of extractResult.enrichments) {
|
|
858
1440
|
enrichmentBuffer.set(`${enr.nodePath}\0${enr.extractorId}`, enr);
|
|
859
1441
|
}
|
|
1442
|
+
for (const c of extractResult.contributions) contributionsBuffer.push(c);
|
|
860
1443
|
const ranAt = Date.now();
|
|
861
1444
|
for (const ex of applicableExtractors) {
|
|
862
1445
|
const qualified = qualifiedExtensionId(ex.pluginId, ex.id);
|
|
@@ -869,6 +1452,7 @@ async function walkAndExtract(opts) {
|
|
|
869
1452
|
}
|
|
870
1453
|
}
|
|
871
1454
|
}
|
|
1455
|
+
const orphanSidecars = discoverOrphanSidecars(roots);
|
|
872
1456
|
return {
|
|
873
1457
|
nodes,
|
|
874
1458
|
internalLinks,
|
|
@@ -877,7 +1461,10 @@ async function walkAndExtract(opts) {
|
|
|
877
1461
|
frontmatterIssues,
|
|
878
1462
|
filesWalked,
|
|
879
1463
|
enrichments: [...enrichmentBuffer.values()],
|
|
880
|
-
extractorRuns
|
|
1464
|
+
extractorRuns,
|
|
1465
|
+
contributions: contributionsBuffer,
|
|
1466
|
+
orphanSidecars,
|
|
1467
|
+
sidecarRoots
|
|
881
1468
|
};
|
|
882
1469
|
}
|
|
883
1470
|
function reuseCachedLink(link, shortIdToQualified, cachedQualifiedIds, applicableQualifiedIds) {
|
|
@@ -906,20 +1493,77 @@ function reuseCachedLink(link, shortIdToQualified, cachedQualifiedIds, applicabl
|
|
|
906
1493
|
if (obsoleteSources.length === 0) return link;
|
|
907
1494
|
return { ...link, sources: cachedSources };
|
|
908
1495
|
}
|
|
909
|
-
async function runRules(rules, nodes, internalLinks, emitter, hookDispatcher) {
|
|
1496
|
+
async function runRules(rules, nodes, internalLinks, orphanSidecars, sidecarRoots, annotationContributions, viewContributions, emitter, hookDispatcher) {
|
|
910
1497
|
const issues = [];
|
|
1498
|
+
const contributions = [];
|
|
1499
|
+
const validators = loadSchemaValidators();
|
|
1500
|
+
const ruleOrphans = orphanSidecars.map((o) => ({
|
|
1501
|
+
relativePath: o.relativePath,
|
|
1502
|
+
expectedMdPath: o.expectedMdPath
|
|
1503
|
+
}));
|
|
911
1504
|
for (const rule of rules) {
|
|
912
|
-
const
|
|
1505
|
+
const qualifiedId = qualifiedExtensionId(rule.pluginId, rule.id);
|
|
1506
|
+
const declaredContributions = readDeclaredContributions(rule);
|
|
1507
|
+
const emitContribution = (nodePath, contributionId, payload) => {
|
|
1508
|
+
const declared = declaredContributions.get(contributionId);
|
|
1509
|
+
if (!declared) {
|
|
1510
|
+
emitExtensionError(emitter, qualifiedId, nodePath, {
|
|
1511
|
+
phase: "emitContribution",
|
|
1512
|
+
contributionId,
|
|
1513
|
+
reason: "unknown-contribution-id",
|
|
1514
|
+
message: tx(ORCHESTRATOR_TEXTS.extensionErrorContributionUnknownId, {
|
|
1515
|
+
extractorId: qualifiedId,
|
|
1516
|
+
contributionId,
|
|
1517
|
+
nodePath
|
|
1518
|
+
})
|
|
1519
|
+
});
|
|
1520
|
+
return;
|
|
1521
|
+
}
|
|
1522
|
+
const result = validators.validateContributionPayload(declared.contract, payload);
|
|
1523
|
+
if (!result.ok) {
|
|
1524
|
+
emitExtensionError(emitter, qualifiedId, nodePath, {
|
|
1525
|
+
phase: "emitContribution",
|
|
1526
|
+
contributionId,
|
|
1527
|
+
contract: declared.contract,
|
|
1528
|
+
reason: result.errors,
|
|
1529
|
+
message: tx(ORCHESTRATOR_TEXTS.extensionErrorContributionPayloadInvalid, {
|
|
1530
|
+
extractorId: qualifiedId,
|
|
1531
|
+
contributionId,
|
|
1532
|
+
nodePath,
|
|
1533
|
+
contract: declared.contract,
|
|
1534
|
+
errors: result.errors
|
|
1535
|
+
})
|
|
1536
|
+
});
|
|
1537
|
+
return;
|
|
1538
|
+
}
|
|
1539
|
+
contributions.push({
|
|
1540
|
+
pluginId: rule.pluginId,
|
|
1541
|
+
extensionId: rule.id,
|
|
1542
|
+
nodePath,
|
|
1543
|
+
contributionId,
|
|
1544
|
+
contract: declared.contract,
|
|
1545
|
+
payload,
|
|
1546
|
+
emittedAt: Date.now()
|
|
1547
|
+
});
|
|
1548
|
+
};
|
|
1549
|
+
const emitted = await rule.evaluate({
|
|
1550
|
+
nodes,
|
|
1551
|
+
links: internalLinks,
|
|
1552
|
+
orphanSidecars: ruleOrphans,
|
|
1553
|
+
sidecarRoots,
|
|
1554
|
+
annotationContributions,
|
|
1555
|
+
viewContributions,
|
|
1556
|
+
emitContribution
|
|
1557
|
+
});
|
|
913
1558
|
for (const issue of emitted) {
|
|
914
1559
|
const validated = validateIssue(rule, issue, emitter);
|
|
915
1560
|
if (validated) issues.push(validated);
|
|
916
1561
|
}
|
|
917
|
-
const
|
|
918
|
-
const evt = makeEvent("rule.completed", { ruleId });
|
|
1562
|
+
const evt = makeEvent("rule.completed", { ruleId: qualifiedId });
|
|
919
1563
|
emitter.emit(evt);
|
|
920
1564
|
await hookDispatcher.dispatch("rule.completed", evt);
|
|
921
1565
|
}
|
|
922
|
-
return issues;
|
|
1566
|
+
return { issues, contributions };
|
|
923
1567
|
}
|
|
924
1568
|
function originatingNodeOf(link, priorNodePaths) {
|
|
925
1569
|
if (link.kind === "supersedes" && !priorNodePaths.has(link.source)) {
|
|
@@ -1090,7 +1734,7 @@ function makeHookDispatcher(hooks, emitter) {
|
|
|
1090
1734
|
await hook.on(ctx);
|
|
1091
1735
|
} catch (err) {
|
|
1092
1736
|
const qualifiedId = qualifiedExtensionId(hook.pluginId, hook.id);
|
|
1093
|
-
const message =
|
|
1737
|
+
const message = formatErrorMessage(err);
|
|
1094
1738
|
emitter.emit(
|
|
1095
1739
|
makeEvent("extension.error", {
|
|
1096
1740
|
kind: "hook-error",
|
|
@@ -1135,7 +1779,6 @@ function buildHookContext(_hook, trigger, event) {
|
|
|
1135
1779
|
function buildNode(args) {
|
|
1136
1780
|
const bytesFrontmatter = Buffer.byteLength(args.frontmatterRaw, "utf8");
|
|
1137
1781
|
const bytesBody = Buffer.byteLength(args.body, "utf8");
|
|
1138
|
-
const metadata = pickMetadata(args.frontmatter);
|
|
1139
1782
|
const node = {
|
|
1140
1783
|
path: args.path,
|
|
1141
1784
|
kind: args.kind,
|
|
@@ -1150,12 +1793,7 @@ function buildNode(args) {
|
|
|
1150
1793
|
linksOutCount: 0,
|
|
1151
1794
|
linksInCount: 0,
|
|
1152
1795
|
externalRefsCount: 0,
|
|
1153
|
-
frontmatter: args.frontmatter
|
|
1154
|
-
title: pickString(args.frontmatter["name"]),
|
|
1155
|
-
description: pickString(args.frontmatter["description"]),
|
|
1156
|
-
stability: pickStability(metadata?.["stability"]),
|
|
1157
|
-
version: pickString(metadata?.["version"]),
|
|
1158
|
-
author: pickString(args.frontmatter["author"])
|
|
1796
|
+
frontmatter: args.frontmatter
|
|
1159
1797
|
};
|
|
1160
1798
|
if (args.encoder) {
|
|
1161
1799
|
node.tokens = countTokens(args.encoder, args.frontmatterRaw, args.body);
|
|
@@ -1176,25 +1814,73 @@ function canonicalFrontmatter(parsed, raw) {
|
|
|
1176
1814
|
if (!hasParsedKeys && hasRawText) {
|
|
1177
1815
|
return raw;
|
|
1178
1816
|
}
|
|
1179
|
-
return
|
|
1817
|
+
return yaml4.dump(parsed, {
|
|
1180
1818
|
sortKeys: true,
|
|
1181
1819
|
lineWidth: -1,
|
|
1182
1820
|
noRefs: true,
|
|
1183
1821
|
noCompatMode: true
|
|
1184
1822
|
});
|
|
1185
1823
|
}
|
|
1186
|
-
function
|
|
1187
|
-
const
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1824
|
+
function resolveAndApplySidecar(node, relativePath, roots, liveBodyHash, liveFrontmatterHash, sidecarRoots) {
|
|
1825
|
+
const issues = [];
|
|
1826
|
+
const mdAbs = resolveAbsoluteMdPath(relativePath, roots);
|
|
1827
|
+
if (mdAbs === null) {
|
|
1828
|
+
node.sidecar = { present: false };
|
|
1829
|
+
return issues;
|
|
1830
|
+
}
|
|
1831
|
+
const result = readSidecarFor(mdAbs);
|
|
1832
|
+
if (!result.present) {
|
|
1833
|
+
node.sidecar = { present: false };
|
|
1834
|
+
return issues;
|
|
1835
|
+
}
|
|
1836
|
+
if (result.parsed === null) {
|
|
1837
|
+
node.sidecar = { present: true, status: null, annotations: null, root: null };
|
|
1838
|
+
for (const parseIssue of result.issues) {
|
|
1839
|
+
issues.push({
|
|
1840
|
+
ruleId: "invalid-sidecar",
|
|
1841
|
+
severity: "warn",
|
|
1842
|
+
nodeIds: [node.path],
|
|
1843
|
+
message: parseIssue.message,
|
|
1844
|
+
data: { sidecarPath: relativePathFromRoots(mdAbs, roots) }
|
|
1845
|
+
});
|
|
1846
|
+
}
|
|
1847
|
+
return issues;
|
|
1848
|
+
}
|
|
1849
|
+
const status = computeDriftStatus({
|
|
1850
|
+
storedBodyHash: result.parsed.identityBodyHash,
|
|
1851
|
+
storedFrontmatterHash: result.parsed.identityFrontmatterHash,
|
|
1852
|
+
liveBodyHash,
|
|
1853
|
+
liveFrontmatterHash
|
|
1854
|
+
});
|
|
1855
|
+
node.sidecar = {
|
|
1856
|
+
present: true,
|
|
1857
|
+
status,
|
|
1858
|
+
annotations: result.parsed.annotations,
|
|
1859
|
+
root: result.parsed.raw
|
|
1860
|
+
};
|
|
1861
|
+
sidecarRoots.set(node.path, result.parsed.raw);
|
|
1862
|
+
return issues;
|
|
1192
1863
|
}
|
|
1193
|
-
function
|
|
1194
|
-
if (
|
|
1864
|
+
function resolveAbsoluteMdPath(relativePath, roots) {
|
|
1865
|
+
if (isAbsolute2(relativePath)) {
|
|
1866
|
+
return existsSync6(relativePath) ? relativePath : null;
|
|
1867
|
+
}
|
|
1868
|
+
for (const root of roots) {
|
|
1869
|
+
const candidate = resolvePath(root, relativePath);
|
|
1870
|
+
if (existsSync6(candidate)) return candidate;
|
|
1871
|
+
}
|
|
1195
1872
|
return null;
|
|
1196
1873
|
}
|
|
1197
|
-
function
|
|
1874
|
+
function relativePathFromRoots(absolutePath, roots) {
|
|
1875
|
+
for (const root of roots) {
|
|
1876
|
+
const abs = resolvePath(root);
|
|
1877
|
+
if (absolutePath.startsWith(`${abs}/`) || absolutePath.startsWith(`${abs}\\`)) {
|
|
1878
|
+
return absolutePath.slice(abs.length + 1).split(/[\\/]/).join("/");
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
return absolutePath;
|
|
1882
|
+
}
|
|
1883
|
+
function buildExtractorContext(extractor, node, body, frontmatter, emitLink, enrichNode, emitContribution, store) {
|
|
1198
1884
|
const scope = extractor.scope;
|
|
1199
1885
|
return {
|
|
1200
1886
|
node,
|
|
@@ -1202,6 +1888,7 @@ function buildExtractorContext(extractor, node, body, frontmatter, emitLink, enr
|
|
|
1202
1888
|
frontmatter: scope === "body" ? {} : frontmatter,
|
|
1203
1889
|
emitLink,
|
|
1204
1890
|
enrichNode,
|
|
1891
|
+
emitContribution,
|
|
1205
1892
|
...store !== void 0 ? { store } : {}
|
|
1206
1893
|
};
|
|
1207
1894
|
}
|
|
@@ -1340,16 +2027,16 @@ function assignSafe(target, source) {
|
|
|
1340
2027
|
}
|
|
1341
2028
|
|
|
1342
2029
|
// kernel/scan/watcher.ts
|
|
1343
|
-
import { resolve as
|
|
2030
|
+
import { resolve as resolve6, relative as relative4, sep as sep3 } from "path";
|
|
1344
2031
|
import chokidar from "chokidar";
|
|
1345
2032
|
function createChokidarWatcher(opts) {
|
|
1346
|
-
const absRoots = opts.roots.map((r) =>
|
|
2033
|
+
const absRoots = opts.roots.map((r) => resolve6(opts.cwd, r));
|
|
1347
2034
|
const ignoreFilterOpt = opts.ignoreFilter;
|
|
1348
2035
|
const getFilter = ignoreFilterOpt === void 0 ? void 0 : typeof ignoreFilterOpt === "function" ? ignoreFilterOpt : () => ignoreFilterOpt;
|
|
1349
2036
|
const ignored = getFilter ? (path) => {
|
|
1350
2037
|
const filter = getFilter();
|
|
1351
2038
|
if (!filter) return false;
|
|
1352
|
-
const rel =
|
|
2039
|
+
const rel = relativePathFromRoots2(path, absRoots);
|
|
1353
2040
|
if (rel === null) return false;
|
|
1354
2041
|
return filter.ignores(rel);
|
|
1355
2042
|
} : void 0;
|
|
@@ -1433,12 +2120,12 @@ function createChokidarWatcher(opts) {
|
|
|
1433
2120
|
};
|
|
1434
2121
|
return { ready, close };
|
|
1435
2122
|
}
|
|
1436
|
-
function
|
|
2123
|
+
function relativePathFromRoots2(absolute, absRoots) {
|
|
1437
2124
|
for (const root of absRoots) {
|
|
1438
|
-
const rel =
|
|
2125
|
+
const rel = relative4(root, absolute);
|
|
1439
2126
|
if (rel === "" || rel === ".") return "";
|
|
1440
|
-
if (!rel.startsWith("..") && !rel.startsWith(`..${
|
|
1441
|
-
return rel.split(
|
|
2127
|
+
if (!rel.startsWith("..") && !rel.startsWith(`..${sep3}`)) {
|
|
2128
|
+
return rel.split(sep3).join("/");
|
|
1442
2129
|
}
|
|
1443
2130
|
}
|
|
1444
2131
|
return null;
|
|
@@ -1689,7 +2376,23 @@ function parseLogLevel(value) {
|
|
|
1689
2376
|
|
|
1690
2377
|
// kernel/index.ts
|
|
1691
2378
|
function createKernel() {
|
|
1692
|
-
|
|
2379
|
+
let annotationKeys = Object.freeze([]);
|
|
2380
|
+
let viewContributions = Object.freeze([]);
|
|
2381
|
+
return {
|
|
2382
|
+
registry: new Registry(),
|
|
2383
|
+
getRegisteredAnnotationKeys() {
|
|
2384
|
+
return annotationKeys;
|
|
2385
|
+
},
|
|
2386
|
+
setRegisteredAnnotationKeys(entries) {
|
|
2387
|
+
annotationKeys = Object.freeze([...entries]);
|
|
2388
|
+
},
|
|
2389
|
+
getRegisteredViewContributions() {
|
|
2390
|
+
return viewContributions;
|
|
2391
|
+
},
|
|
2392
|
+
setRegisteredViewContributions(entries) {
|
|
2393
|
+
viewContributions = Object.freeze([...entries]);
|
|
2394
|
+
}
|
|
2395
|
+
};
|
|
1693
2396
|
}
|
|
1694
2397
|
export {
|
|
1695
2398
|
DuplicateExtensionError,
|