@orkestrel/scaffold 0.0.100 → 0.0.102
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/agents/skills/orkestrel-dispatch/scripts/brief.js +6 -14
- package/dist/agents/skills/orkestrel-dispatch/scripts/cite.js +41 -28
- package/dist/agents/skills/orkestrel-dispatch/scripts/helpers.js +5 -12
- package/dist/agents/skills/orkestrel-dispatch/scripts/launch.js +25 -13
- package/dist/agents/skills/orkestrel-dispatch/scripts/login.js +18 -25
- package/dist/agents/skills/orkestrel-dispatch/scripts/result.js +11 -9
- package/dist/agents/skills/orkestrel-harden/scripts/discovery.js +233 -232
- package/dist/agents/skills/orkestrel-publish/scripts/compare.js +95 -89
- package/dist/agents/skills/orkestrel-publish/scripts/pins.js +5 -7
- package/dist/agents/skills/orkestrel-publish/scripts/swap.js +1273 -0
- package/dist/agents/skills/orkestrel-publish/scripts/wave.js +99 -89
- package/dist/agents/skills/orkestrel-publish/scripts/window.js +54 -52
- package/dist/agents/skills/orkestrel-scout/scripts/map.js +160 -161
- package/dist/bin/main.js +483 -444
- package/dist/bin/main.js.map +1 -1
- package/dist/host/AGENTS.md +1 -0
- package/dist/host/agents/skills/orkestrel-dispatch/SKILL.md +10 -10
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/brief.ts +8 -11
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/cite.ts +58 -28
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/helpers.ts +5 -10
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/launch.ts +31 -12
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/login.ts +28 -23
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/result.ts +12 -10
- package/dist/host/agents/skills/orkestrel-harden/scripts/discovery.ts +318 -237
- package/dist/host/agents/skills/orkestrel-journey/references/statechart.md +2 -5
- package/dist/host/agents/skills/orkestrel-publish/SKILL.md +7 -6
- package/dist/host/agents/skills/orkestrel-publish/references/release.md +12 -10
- package/dist/host/agents/skills/orkestrel-publish/references/wave.md +12 -5
- package/dist/host/agents/skills/orkestrel-publish/scripts/compare.ts +124 -82
- package/dist/host/agents/skills/orkestrel-publish/scripts/pins.ts +5 -7
- package/dist/host/agents/skills/orkestrel-publish/scripts/swap.ts +1655 -0
- package/dist/host/agents/skills/orkestrel-publish/scripts/wave.ts +128 -97
- package/dist/host/agents/skills/orkestrel-publish/scripts/window.ts +70 -55
- package/dist/host/agents/skills/orkestrel-scout/scripts/map.ts +215 -155
- package/dist/host/claude/rules/architecture.md +2 -2
- package/dist/host/claude/rules/tests.md +8 -2
- package/dist/host/claude/rules/typescript.md +130 -0
- package/dist/host/claude/rules/workspace.md +36 -12
- package/dist/host/configs/helpers.ts +214 -133
- package/dist/host/configs/policy.ts +110 -105
- package/dist/host/dotfiles/gitignore +1 -0
- package/dist/host/dotfiles/oxlintrc.json +10 -0
- package/dist/host/guides/scaffold.md +244 -170
- package/dist/host/manifest.json +37 -31
- package/dist/host/tests/config.test.ts +1305 -619
- package/dist/host/tests/policy.test.ts +19 -0
- package/dist/host/tests/setupPolicy.ts +1710 -995
- package/dist/src/core/index.cjs +1595 -1686
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +202 -18
- package/dist/src/core/index.d.ts +202 -18
- package/dist/src/core/index.js +1590 -1687
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +448 -392
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +1182 -1052
- package/dist/src/server/index.d.ts +1182 -1052
- package/dist/src/server/index.js +447 -395
- package/dist/src/server/index.js.map +1 -1
- package/package.json +2 -2
|
@@ -10,13 +10,16 @@ import { spawnSync } from 'node:child_process'
|
|
|
10
10
|
import { createRequire } from 'node:module'
|
|
11
11
|
import {
|
|
12
12
|
realpathSync,
|
|
13
|
+
chmodSync,
|
|
13
14
|
existsSync,
|
|
14
15
|
globSync,
|
|
16
|
+
lstatSync,
|
|
15
17
|
mkdirSync,
|
|
16
18
|
mkdtempSync,
|
|
17
19
|
readFileSync,
|
|
18
20
|
readdirSync,
|
|
19
21
|
rmSync,
|
|
22
|
+
statSync,
|
|
20
23
|
writeFileSync,
|
|
21
24
|
} from 'node:fs'
|
|
22
25
|
import { tmpdir } from 'node:os'
|
|
@@ -160,6 +163,15 @@ export function createPolicySurfaceFixture(): PolicyScratchInterface {
|
|
|
160
163
|
}
|
|
161
164
|
}
|
|
162
165
|
|
|
166
|
+
/** Describes the include, setup files, and probe settings one workspace project registers. */
|
|
167
|
+
export interface PolicyProject {
|
|
168
|
+
readonly benchmark?: readonly string[]
|
|
169
|
+
readonly include: string | readonly string[]
|
|
170
|
+
readonly parallel?: boolean
|
|
171
|
+
readonly pool?: string
|
|
172
|
+
readonly setup: readonly string[]
|
|
173
|
+
}
|
|
174
|
+
|
|
163
175
|
/** Describes one physical negative control, including the population boundary it attacks. */
|
|
164
176
|
export interface PolicyControl {
|
|
165
177
|
readonly label: string
|
|
@@ -212,6 +224,513 @@ export function createPolicyScratch(options: {
|
|
|
212
224
|
}
|
|
213
225
|
}
|
|
214
226
|
|
|
227
|
+
/**
|
|
228
|
+
* Creates a list with the given length that holds only the entries written at their indexes.
|
|
229
|
+
*
|
|
230
|
+
* @remarks
|
|
231
|
+
* A literal cannot express a hole, so the list is sized first and then written once per index. A
|
|
232
|
+
* `for…of` loop and a spread visit each hole as `undefined`, where `map`, `filter`, and `flatMap`
|
|
233
|
+
* skip it, so a caller-supplied list built here separates the two.
|
|
234
|
+
*
|
|
235
|
+
* @param length - The length of the list, holes included.
|
|
236
|
+
* @param entries - The entry to write at each index; every other index stays a hole.
|
|
237
|
+
* @returns The sparse list.
|
|
238
|
+
*/
|
|
239
|
+
export function createPolicySparse<T>(length: number, entries: ReadonlyMap<number, T>): T[] {
|
|
240
|
+
const list: T[] = []
|
|
241
|
+
list.length = length
|
|
242
|
+
for (const [index, entry] of entries) list[index] = entry
|
|
243
|
+
return list
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Creates a list whose entries are getters, so a reader's every read of an entry is recorded.
|
|
248
|
+
*
|
|
249
|
+
* @remarks
|
|
250
|
+
* Only the indexes in `entries` hold a getter, and every other index stays a hole. A getter reports
|
|
251
|
+
* its index to `record` and then produces its value, which may throw, so a case sees which entries
|
|
252
|
+
* a reader touched before it stopped. An iterator reads an entry once as it yields it, where a copy
|
|
253
|
+
* of the list reads every entry before the reader sees the first.
|
|
254
|
+
*
|
|
255
|
+
* @param length - The length of the list, holes included.
|
|
256
|
+
* @param entries - The producer behind each getter, by index.
|
|
257
|
+
* @param record - The function that receives an index each time its getter runs.
|
|
258
|
+
* @param iterable - If `true`, the list keeps the iterator every array has; if `false`, its
|
|
259
|
+
* `Symbol.iterator` is `undefined`, which a loop refuses and `Array.from` and `values()` do not.
|
|
260
|
+
* Default: `true`.
|
|
261
|
+
* @returns The list.
|
|
262
|
+
*/
|
|
263
|
+
export function createPolicyReads<T>(
|
|
264
|
+
length: number,
|
|
265
|
+
entries: ReadonlyMap<number, () => T>,
|
|
266
|
+
record: (index: number) => void,
|
|
267
|
+
iterable = true,
|
|
268
|
+
): T[] {
|
|
269
|
+
const list = createPolicySparse<T>(length, new Map())
|
|
270
|
+
for (const [index, produce] of entries) {
|
|
271
|
+
Object.defineProperty(list, index, {
|
|
272
|
+
configurable: true,
|
|
273
|
+
enumerable: true,
|
|
274
|
+
get() {
|
|
275
|
+
record(index)
|
|
276
|
+
return produce()
|
|
277
|
+
},
|
|
278
|
+
})
|
|
279
|
+
}
|
|
280
|
+
if (!iterable) Object.defineProperty(list, Symbol.iterator, { value: undefined })
|
|
281
|
+
return list
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Throws the error a getter raises when its entry refuses to be read.
|
|
286
|
+
*
|
|
287
|
+
* @returns Never.
|
|
288
|
+
* @throws RangeError - Thrown when called.
|
|
289
|
+
*/
|
|
290
|
+
export function failPolicyRead(): never {
|
|
291
|
+
throw new RangeError('The entry refused its read')
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Settles one action into text a case compares for equality.
|
|
296
|
+
*
|
|
297
|
+
* @param action - The action to run.
|
|
298
|
+
* @returns `return` and the JSON of the returned value, or `throw` and the constructor name.
|
|
299
|
+
*/
|
|
300
|
+
export function settlePolicyOutcome(action: () => unknown): string {
|
|
301
|
+
try {
|
|
302
|
+
return `return ${JSON.stringify(action())}`
|
|
303
|
+
} catch (error) {
|
|
304
|
+
return `throw ${error instanceof Error ? error.constructor.name : typeof error}`
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** Describes one reader run over a recorded list, with the outcome and entry reads it must show. */
|
|
309
|
+
export interface PolicyLazyCase {
|
|
310
|
+
readonly label: string
|
|
311
|
+
readonly outcome: string
|
|
312
|
+
readonly reads: readonly number[]
|
|
313
|
+
readonly observe: (record: (index: number) => void) => string
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Creates one case that runs a reader over a recorded list.
|
|
318
|
+
*
|
|
319
|
+
* @param label - The text naming the reader and the list.
|
|
320
|
+
* @param reader - The reader to run over the list.
|
|
321
|
+
* @param list - The length of the list and the producer behind each getter, by index.
|
|
322
|
+
* @param expected - The settled outcome and the indexes the reader must read, in order.
|
|
323
|
+
* @returns The case.
|
|
324
|
+
*/
|
|
325
|
+
export function createPolicyLazyCase<T>(
|
|
326
|
+
label: string,
|
|
327
|
+
reader: (list: T[]) => unknown,
|
|
328
|
+
list: {
|
|
329
|
+
readonly length: number
|
|
330
|
+
readonly entries: ReadonlyMap<number, () => T>
|
|
331
|
+
readonly iterable?: boolean
|
|
332
|
+
},
|
|
333
|
+
expected: { readonly outcome: string; readonly reads: readonly number[] },
|
|
334
|
+
): PolicyLazyCase {
|
|
335
|
+
return {
|
|
336
|
+
label,
|
|
337
|
+
outcome: expected.outcome,
|
|
338
|
+
reads: expected.reads,
|
|
339
|
+
observe: (record) =>
|
|
340
|
+
settlePolicyOutcome(() =>
|
|
341
|
+
reader(createPolicyReads(list.length, list.entries, record, list.iterable)),
|
|
342
|
+
),
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Describes the entries of one recorded list.
|
|
348
|
+
*
|
|
349
|
+
* @remarks
|
|
350
|
+
* An index in `entries` produces a value the reader accepts, an index in `refusals` produces a
|
|
351
|
+
* value the reader fails on once it has the value, and an index in `failures` fails when the
|
|
352
|
+
* getter runs. Every other index is a hole. A list with `iterable` set to `false` has no iterator,
|
|
353
|
+
* which a loop refuses and `Array.from` and `values()` do not; it is an iterator list otherwise.
|
|
354
|
+
*/
|
|
355
|
+
export interface PolicyLazyShape {
|
|
356
|
+
readonly label: string
|
|
357
|
+
readonly length: number
|
|
358
|
+
readonly entries: readonly number[]
|
|
359
|
+
readonly refusals: readonly number[]
|
|
360
|
+
readonly failures: readonly number[]
|
|
361
|
+
readonly iterable?: boolean
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/** Holds the list shapes that tell one-entry-at-a-time reading from a copy that reads ahead. */
|
|
365
|
+
export const POLICY_LAZY_SHAPES = Object.freeze({
|
|
366
|
+
empty: { label: 'an empty list', length: 0, entries: [], refusals: [], failures: [] },
|
|
367
|
+
dense: { label: 'one entry', length: 1, entries: [0], refusals: [], failures: [] },
|
|
368
|
+
hole: { label: 'a hole, then an entry', length: 2, entries: [1], refusals: [], failures: [] },
|
|
369
|
+
holeFailure: {
|
|
370
|
+
label: 'a hole, then a failing entry',
|
|
371
|
+
length: 2,
|
|
372
|
+
entries: [],
|
|
373
|
+
refusals: [],
|
|
374
|
+
failures: [1],
|
|
375
|
+
},
|
|
376
|
+
failureFirst: {
|
|
377
|
+
label: 'a failing entry, then an entry',
|
|
378
|
+
length: 2,
|
|
379
|
+
entries: [1],
|
|
380
|
+
refusals: [],
|
|
381
|
+
failures: [0],
|
|
382
|
+
},
|
|
383
|
+
refusalFirst: {
|
|
384
|
+
label: 'a refused entry, then an entry',
|
|
385
|
+
length: 2,
|
|
386
|
+
entries: [1],
|
|
387
|
+
refusals: [0],
|
|
388
|
+
failures: [],
|
|
389
|
+
},
|
|
390
|
+
holeBetween: {
|
|
391
|
+
label: 'an entry, a hole, then an entry',
|
|
392
|
+
length: 3,
|
|
393
|
+
entries: [0, 2],
|
|
394
|
+
refusals: [],
|
|
395
|
+
failures: [],
|
|
396
|
+
},
|
|
397
|
+
unlisted: {
|
|
398
|
+
label: 'two entries on a list without an iterator',
|
|
399
|
+
length: 2,
|
|
400
|
+
entries: [0, 1],
|
|
401
|
+
refusals: [],
|
|
402
|
+
failures: [],
|
|
403
|
+
iterable: false,
|
|
404
|
+
},
|
|
405
|
+
} satisfies Record<string, PolicyLazyShape>)
|
|
406
|
+
|
|
407
|
+
/** Holds values a reader's signature refuses, which a case feeds it to make processing fail. */
|
|
408
|
+
export const POLICY_REFUSALS = Object.freeze({ number: 7, symbol: Symbol('refused') })
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Creates a revoked proxy, which refuses every guard that inspects it.
|
|
412
|
+
*
|
|
413
|
+
* @returns The proxy, whose `Array.isArray` check throws a `TypeError`.
|
|
414
|
+
*/
|
|
415
|
+
export function createPolicyRevoked(): object {
|
|
416
|
+
const { proxy, revoke } = Proxy.revocable({}, {})
|
|
417
|
+
revoke()
|
|
418
|
+
return proxy
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Creates one case per list shape for a reader of a caller-supplied list.
|
|
423
|
+
*
|
|
424
|
+
* @param subject - The text naming the reader.
|
|
425
|
+
* @param reader - The reader to run over each list.
|
|
426
|
+
* @param producers - The producer behind each entry the reader accepts, and behind each it refuses.
|
|
427
|
+
* @param rows - Each shape with the settled outcome and the indexes the reader must read, in order.
|
|
428
|
+
* @returns One case per row.
|
|
429
|
+
*/
|
|
430
|
+
export function createPolicyLazyCases<T>(
|
|
431
|
+
subject: string,
|
|
432
|
+
reader: (list: T[]) => unknown,
|
|
433
|
+
producers: { readonly valid: () => T; readonly refusal: () => T },
|
|
434
|
+
rows: ReadonlyArray<readonly [PolicyLazyShape, string, readonly number[]]>,
|
|
435
|
+
): readonly PolicyLazyCase[] {
|
|
436
|
+
return rows.map(([shape, outcome, reads]) =>
|
|
437
|
+
createPolicyLazyCase(
|
|
438
|
+
`${subject}: ${shape.label}`,
|
|
439
|
+
reader,
|
|
440
|
+
{
|
|
441
|
+
length: shape.length,
|
|
442
|
+
entries: new Map<number, () => T>([
|
|
443
|
+
...shape.entries.map((index): [number, () => T] => [index, producers.valid]),
|
|
444
|
+
...shape.refusals.map((index): [number, () => T] => [index, producers.refusal]),
|
|
445
|
+
...shape.failures.map((index): [number, () => T] => [index, failPolicyRead]),
|
|
446
|
+
]),
|
|
447
|
+
iterable: shape.iterable ?? true,
|
|
448
|
+
},
|
|
449
|
+
{ outcome, reads },
|
|
450
|
+
),
|
|
451
|
+
)
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Holds one case per reader and list shape for the readers that take a caller-supplied list.
|
|
456
|
+
*
|
|
457
|
+
* @remarks
|
|
458
|
+
* Each outcome and read list is what the loop each reader replaced produced. A hole is visited as
|
|
459
|
+
* `undefined`, an entry after a failing one is never read, a refusal names its constructor, and a
|
|
460
|
+
* list without an iterator is refused with a `TypeError` before any entry is read.
|
|
461
|
+
*/
|
|
462
|
+
export const POLICY_LAZY_CASES: readonly PolicyLazyCase[] = Object.freeze([
|
|
463
|
+
...createPolicyLazyCases(
|
|
464
|
+
'inspectPolicyMirrorPaths',
|
|
465
|
+
(list: string[]) => inspectPolicyMirrorPaths(list, new Set()).map(({ path }) => path),
|
|
466
|
+
{
|
|
467
|
+
valid: () => 'tests/src/core/A.test.ts',
|
|
468
|
+
// The descriptor read answers `any`, so a value the signature refuses reaches the reader.
|
|
469
|
+
refusal: () => Object.getOwnPropertyDescriptor(POLICY_REFUSALS, 'symbol')?.value,
|
|
470
|
+
},
|
|
471
|
+
[
|
|
472
|
+
[POLICY_LAZY_SHAPES.empty, 'return []', []],
|
|
473
|
+
[POLICY_LAZY_SHAPES.dense, 'return ["tests/src/core/A.test.ts"]', [0]],
|
|
474
|
+
[POLICY_LAZY_SHAPES.hole, 'throw TypeError', []],
|
|
475
|
+
[POLICY_LAZY_SHAPES.holeFailure, 'throw TypeError', []],
|
|
476
|
+
[POLICY_LAZY_SHAPES.failureFirst, 'throw RangeError', [0]],
|
|
477
|
+
[POLICY_LAZY_SHAPES.refusalFirst, 'throw TypeError', [0]],
|
|
478
|
+
[POLICY_LAZY_SHAPES.holeBetween, 'throw TypeError', [0]],
|
|
479
|
+
[POLICY_LAZY_SHAPES.unlisted, 'throw TypeError', []],
|
|
480
|
+
],
|
|
481
|
+
),
|
|
482
|
+
...createPolicyLazyCases(
|
|
483
|
+
'inspectPolicyWiring rules',
|
|
484
|
+
(list: string[]) => inspectPolicyWiring({}, list, []),
|
|
485
|
+
{
|
|
486
|
+
valid: () => 'a',
|
|
487
|
+
refusal: () => Object.getOwnPropertyDescriptor(POLICY_REFUSALS, 'symbol')?.value,
|
|
488
|
+
},
|
|
489
|
+
[
|
|
490
|
+
[POLICY_LAZY_SHAPES.empty, 'return []', []],
|
|
491
|
+
[
|
|
492
|
+
POLICY_LAZY_SHAPES.dense,
|
|
493
|
+
'return ["a is enabled by no top-level or override rules record"]',
|
|
494
|
+
[0],
|
|
495
|
+
],
|
|
496
|
+
[
|
|
497
|
+
POLICY_LAZY_SHAPES.hole,
|
|
498
|
+
'return ["undefined is enabled by no top-level or override rules record","a is enabled by no top-level or override rules record"]',
|
|
499
|
+
[1],
|
|
500
|
+
],
|
|
501
|
+
[POLICY_LAZY_SHAPES.holeFailure, 'throw RangeError', [1]],
|
|
502
|
+
[POLICY_LAZY_SHAPES.failureFirst, 'throw RangeError', [0]],
|
|
503
|
+
[POLICY_LAZY_SHAPES.refusalFirst, 'throw TypeError', [0]],
|
|
504
|
+
[
|
|
505
|
+
POLICY_LAZY_SHAPES.holeBetween,
|
|
506
|
+
'return ["a is enabled by no top-level or override rules record","undefined is enabled by no top-level or override rules record","a is enabled by no top-level or override rules record"]',
|
|
507
|
+
[0, 2],
|
|
508
|
+
],
|
|
509
|
+
[POLICY_LAZY_SHAPES.unlisted, 'throw TypeError', []],
|
|
510
|
+
],
|
|
511
|
+
),
|
|
512
|
+
...createPolicyLazyCases(
|
|
513
|
+
'inspectPolicyWiring populations',
|
|
514
|
+
(list: Array<readonly string[]>) => inspectPolicyWiring({}, [], list),
|
|
515
|
+
{
|
|
516
|
+
valid: () => ['a'],
|
|
517
|
+
refusal: () => Object.getOwnPropertyDescriptor(POLICY_REFUSALS, 'number')?.value,
|
|
518
|
+
},
|
|
519
|
+
[
|
|
520
|
+
[POLICY_LAZY_SHAPES.empty, 'return []', []],
|
|
521
|
+
[
|
|
522
|
+
POLICY_LAZY_SHAPES.dense,
|
|
523
|
+
'return ["no override files list declares the population exactly: a"]',
|
|
524
|
+
[0],
|
|
525
|
+
],
|
|
526
|
+
[POLICY_LAZY_SHAPES.hole, 'throw TypeError', []],
|
|
527
|
+
[POLICY_LAZY_SHAPES.holeFailure, 'throw TypeError', []],
|
|
528
|
+
[POLICY_LAZY_SHAPES.failureFirst, 'throw RangeError', [0]],
|
|
529
|
+
[POLICY_LAZY_SHAPES.refusalFirst, 'throw TypeError', [0]],
|
|
530
|
+
[POLICY_LAZY_SHAPES.holeBetween, 'throw TypeError', [0]],
|
|
531
|
+
[POLICY_LAZY_SHAPES.unlisted, 'throw TypeError', []],
|
|
532
|
+
],
|
|
533
|
+
),
|
|
534
|
+
...createPolicyLazyCases(
|
|
535
|
+
'inspectPolicyWiring overrides',
|
|
536
|
+
(list: unknown[]) => inspectPolicyWiring({ overrides: list }, [], []),
|
|
537
|
+
{ valid: () => ({ files: ['a'] }), refusal: createPolicyRevoked },
|
|
538
|
+
[
|
|
539
|
+
[POLICY_LAZY_SHAPES.empty, 'return []', []],
|
|
540
|
+
[POLICY_LAZY_SHAPES.dense, 'return []', [0]],
|
|
541
|
+
[POLICY_LAZY_SHAPES.hole, 'return []', [1]],
|
|
542
|
+
[POLICY_LAZY_SHAPES.holeFailure, 'throw RangeError', [1]],
|
|
543
|
+
[POLICY_LAZY_SHAPES.failureFirst, 'throw RangeError', [0]],
|
|
544
|
+
[POLICY_LAZY_SHAPES.refusalFirst, 'throw TypeError', [0]],
|
|
545
|
+
[POLICY_LAZY_SHAPES.holeBetween, 'return []', [0, 2]],
|
|
546
|
+
[POLICY_LAZY_SHAPES.unlisted, 'throw TypeError', []],
|
|
547
|
+
],
|
|
548
|
+
),
|
|
549
|
+
...createPolicyLazyCases(
|
|
550
|
+
'inspectPolicyConfiguration overrides',
|
|
551
|
+
(list: unknown[]) =>
|
|
552
|
+
inspectPolicyConfiguration({
|
|
553
|
+
rules: Object.fromEntries(POLICY_WIRING_RULES.map((rule) => [rule, 'error'])),
|
|
554
|
+
overrides: list,
|
|
555
|
+
}),
|
|
556
|
+
{ valid: () => ({}), refusal: createPolicyRevoked },
|
|
557
|
+
[
|
|
558
|
+
[POLICY_LAZY_SHAPES.empty, 'return []', []],
|
|
559
|
+
[POLICY_LAZY_SHAPES.dense, 'return []', [0]],
|
|
560
|
+
[POLICY_LAZY_SHAPES.hole, 'return ["override entries must be records"]', [1]],
|
|
561
|
+
[POLICY_LAZY_SHAPES.holeFailure, 'throw RangeError', [1]],
|
|
562
|
+
[POLICY_LAZY_SHAPES.failureFirst, 'throw RangeError', [0]],
|
|
563
|
+
[POLICY_LAZY_SHAPES.refusalFirst, 'throw TypeError', [0]],
|
|
564
|
+
[POLICY_LAZY_SHAPES.holeBetween, 'return ["override entries must be records"]', [0, 2]],
|
|
565
|
+
[POLICY_LAZY_SHAPES.unlisted, 'throw TypeError', []],
|
|
566
|
+
],
|
|
567
|
+
),
|
|
568
|
+
...createPolicyLazyCases(
|
|
569
|
+
'inspectPolicyConfiguration ignorePatterns',
|
|
570
|
+
(list: unknown[]) =>
|
|
571
|
+
inspectPolicyConfiguration({
|
|
572
|
+
rules: Object.fromEntries(POLICY_WIRING_RULES.map((rule) => [rule, 'error'])),
|
|
573
|
+
ignorePatterns: list,
|
|
574
|
+
}),
|
|
575
|
+
// The reader skips every entry that is not a string, so no entry refuses it and no row uses the
|
|
576
|
+
// refusal.
|
|
577
|
+
{ valid: () => 'src', refusal: createPolicyRevoked },
|
|
578
|
+
[
|
|
579
|
+
[POLICY_LAZY_SHAPES.empty, 'return []', []],
|
|
580
|
+
[POLICY_LAZY_SHAPES.dense, 'return ["ignorePatterns must not reach src"]', [0]],
|
|
581
|
+
[POLICY_LAZY_SHAPES.hole, 'return ["ignorePatterns must not reach src"]', [1]],
|
|
582
|
+
[POLICY_LAZY_SHAPES.holeFailure, 'throw RangeError', [1]],
|
|
583
|
+
[POLICY_LAZY_SHAPES.failureFirst, 'throw RangeError', [0]],
|
|
584
|
+
[
|
|
585
|
+
POLICY_LAZY_SHAPES.holeBetween,
|
|
586
|
+
'return ["ignorePatterns must not reach src","ignorePatterns must not reach src"]',
|
|
587
|
+
[0, 2],
|
|
588
|
+
],
|
|
589
|
+
[POLICY_LAZY_SHAPES.unlisted, 'throw TypeError', []],
|
|
590
|
+
],
|
|
591
|
+
),
|
|
592
|
+
...createPolicyLazyCases(
|
|
593
|
+
'inspectPolicyFilenamePaths',
|
|
594
|
+
(list: string[]) => inspectPolicyFilenamePaths(list).map(({ path }) => path),
|
|
595
|
+
{
|
|
596
|
+
valid: () => 'src/a.ts',
|
|
597
|
+
refusal: () => Object.getOwnPropertyDescriptor(POLICY_REFUSALS, 'symbol')?.value,
|
|
598
|
+
},
|
|
599
|
+
[[POLICY_LAZY_SHAPES.unlisted, 'throw TypeError', []]],
|
|
600
|
+
),
|
|
601
|
+
])
|
|
602
|
+
|
|
603
|
+
/** Describes a population a guide states, the words that mark a sentence stating it, and its exclusion. */
|
|
604
|
+
export interface PolicyPopulation {
|
|
605
|
+
readonly subject: string
|
|
606
|
+
readonly markers: readonly string[]
|
|
607
|
+
readonly exclusion: string
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/** Describes the population of test files that selects the `studies` fact, and what it leaves out. */
|
|
611
|
+
export const POLICY_STUDIES_POPULATION: PolicyPopulation = Object.freeze({
|
|
612
|
+
subject: '`studies`',
|
|
613
|
+
markers: Object.freeze(['`*.test.ts`', 'test files']),
|
|
614
|
+
exclusion: 'outside any `node_modules` or `.git` directory',
|
|
615
|
+
})
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Collects the guide prose sentences that state one population.
|
|
619
|
+
*
|
|
620
|
+
* @remarks
|
|
621
|
+
* A sentence states the population when it names the subject and one marker. A fenced code block
|
|
622
|
+
* is not prose. The lines of a block that are not table rows are joined into one text, and each
|
|
623
|
+
* cell of a table row is a text of its own, so a `Summary` cell that equals its export's
|
|
624
|
+
* description is held to the same sentences as the prose: the source doc block is where that cell's
|
|
625
|
+
* wording is decided.
|
|
626
|
+
*
|
|
627
|
+
* @param markdown - The guide Markdown.
|
|
628
|
+
* @param population - The subject, markers, and exclusion that describe the population.
|
|
629
|
+
* @returns Each sentence that states the population, in document order.
|
|
630
|
+
*/
|
|
631
|
+
export function collectGuideStatements(
|
|
632
|
+
markdown: string,
|
|
633
|
+
population: PolicyPopulation,
|
|
634
|
+
): readonly string[] {
|
|
635
|
+
return markdown
|
|
636
|
+
.replaceAll(/^```[^\n]*\n[\s\S]*?^```[^\n]*$/gmu, '')
|
|
637
|
+
.split(/(?:\r\n|\n){2,}/u)
|
|
638
|
+
.flatMap((block) => {
|
|
639
|
+
const lines = block.split(/\r\n|\n/u)
|
|
640
|
+
return [
|
|
641
|
+
lines.filter((line) => !line.startsWith('|')).join(' '),
|
|
642
|
+
...lines.filter((line) => line.startsWith('|')).flatMap((row) => row.split(/(?<!\\)\|/u)),
|
|
643
|
+
].flatMap((text) =>
|
|
644
|
+
text
|
|
645
|
+
.replaceAll(/\s+/gu, ' ')
|
|
646
|
+
.trim()
|
|
647
|
+
.split(/(?<=[.!?])\s+/u),
|
|
648
|
+
)
|
|
649
|
+
})
|
|
650
|
+
.filter(
|
|
651
|
+
(sentence) =>
|
|
652
|
+
sentence.includes(population.subject) &&
|
|
653
|
+
population.markers.some((marker) => sentence.includes(marker)),
|
|
654
|
+
)
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/** Names the security identifier of the group every access token holds, in the form `icacls` accepts. */
|
|
658
|
+
export const POLICY_DENY_PRINCIPAL = '*S-1-1-0'
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* Runs one executable to completion and returns what it printed.
|
|
662
|
+
*
|
|
663
|
+
* @param file - The executable to run, resolved by name.
|
|
664
|
+
* @param args - The arguments, passed without a shell.
|
|
665
|
+
* @returns The standard output.
|
|
666
|
+
* @throws Error - Thrown when the executable cannot start or exits with a non-zero status.
|
|
667
|
+
*/
|
|
668
|
+
export function runPolicyCommand(file: string, args: readonly string[]): string {
|
|
669
|
+
const result = spawnSync(file, args, { encoding: 'utf8', windowsHide: true })
|
|
670
|
+
if (result.error !== undefined) throw result.error
|
|
671
|
+
if (result.status !== 0)
|
|
672
|
+
throw new Error(
|
|
673
|
+
`${file} ${args.join(' ')} exited with ${result.status}: ${result.stdout}${result.stderr}`,
|
|
674
|
+
)
|
|
675
|
+
return result.stdout
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* Reads the permission state of one file as text two readings compare for equality.
|
|
680
|
+
*
|
|
681
|
+
* @remarks
|
|
682
|
+
* Permission state is platform-defined. A POSIX file carries a mode, read as its octal digits
|
|
683
|
+
* through `statSync`, and a Windows file carries an access-control list, read as the `icacls`
|
|
684
|
+
* listing of the path, so the text holds every explicit and inherited entry in order.
|
|
685
|
+
*
|
|
686
|
+
* @param path - The absolute path of an existing file.
|
|
687
|
+
* @returns The octal mode on POSIX, and the `icacls` listing on Windows.
|
|
688
|
+
* @throws Error - Thrown when the Windows listing command exits with a non-zero status.
|
|
689
|
+
*/
|
|
690
|
+
export function readPolicyPermissions(path: string): string {
|
|
691
|
+
return process.platform === 'win32'
|
|
692
|
+
? runPolicyCommand('icacls', [path])
|
|
693
|
+
: (statSync(path).mode & 0o7777).toString(8)
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* Withholds read access to one file from the current user.
|
|
698
|
+
*
|
|
699
|
+
* @remarks
|
|
700
|
+
* Read permission is platform-defined. A POSIX mode of zero withholds it from every user but root,
|
|
701
|
+
* and a Windows access-control entry that denies read withholds it from the holder of every token.
|
|
702
|
+
* A `package.json` that is a directory cannot stand in for this: the regular-file check refuses it
|
|
703
|
+
* before any read starts, so only a file the host refuses to open reaches the read boundary.
|
|
704
|
+
*
|
|
705
|
+
* The restore returns the permission state it found. A POSIX restore writes back the mode read
|
|
706
|
+
* before the change. A Windows restore removes the deny entries of the one principal the denial
|
|
707
|
+
* names, because `icacls /restore` of a saved list needs a restore privilege a standard token does
|
|
708
|
+
* not hold, and `/remove:d` of the user would also strip a deny entry the file already carried. A
|
|
709
|
+
* restore that leaves the state different, as an earlier deny entry for that principal would,
|
|
710
|
+
* throws rather than report success.
|
|
711
|
+
*
|
|
712
|
+
* @param path - The absolute path of an existing file.
|
|
713
|
+
* @returns A function that restores the access, to run before the owning directory is removed.
|
|
714
|
+
* @throws Error - Thrown when a command exits with a non-zero status or the restore differs.
|
|
715
|
+
*/
|
|
716
|
+
export function denyPolicyRead(path: string): () => void {
|
|
717
|
+
const before = readPolicyPermissions(path)
|
|
718
|
+
if (process.platform === 'win32') {
|
|
719
|
+
runPolicyCommand('icacls', [path, '/deny', `${POLICY_DENY_PRINCIPAL}:(R)`])
|
|
720
|
+
} else {
|
|
721
|
+
chmodSync(path, 0o000)
|
|
722
|
+
}
|
|
723
|
+
return () => {
|
|
724
|
+
if (process.platform === 'win32') {
|
|
725
|
+
runPolicyCommand('icacls', [path, '/remove:d', POLICY_DENY_PRINCIPAL])
|
|
726
|
+
} else {
|
|
727
|
+
chmodSync(path, before)
|
|
728
|
+
}
|
|
729
|
+
if (readPolicyPermissions(path) !== before)
|
|
730
|
+
throw new Error(`The permissions of ${path} did not restore exactly`)
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
215
734
|
/** Holds parsed skill frontmatter and the exact scalar source used for bridge comparison. */
|
|
216
735
|
export interface SkillFrontmatter {
|
|
217
736
|
readonly keys: readonly string[]
|
|
@@ -223,6 +742,15 @@ export interface SkillFrontmatter {
|
|
|
223
742
|
}
|
|
224
743
|
}
|
|
225
744
|
|
|
745
|
+
/** Holds the keys and fields read so far from one frontmatter and the last line a folded block consumed. */
|
|
746
|
+
export interface SkillFrontmatterScan {
|
|
747
|
+
readonly keys: readonly string[]
|
|
748
|
+
readonly skipThrough: number
|
|
749
|
+
readonly fields: ReadonlyArray<
|
|
750
|
+
readonly [string, { readonly value: string; readonly source: string }]
|
|
751
|
+
>
|
|
752
|
+
}
|
|
753
|
+
|
|
226
754
|
/** Names the directory whose immediate child directories form the complete skill family. */
|
|
227
755
|
export const SKILL_FAMILY_ROOT = '.agents/skills'
|
|
228
756
|
|
|
@@ -436,6 +964,51 @@ export function isPolicyRecord(value: unknown): value is Readonly<Record<string,
|
|
|
436
964
|
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
437
965
|
}
|
|
438
966
|
|
|
967
|
+
/**
|
|
968
|
+
* Parses JSON text without throwing.
|
|
969
|
+
*
|
|
970
|
+
* @param content - The raw JSON text.
|
|
971
|
+
* @returns The parsed value, or `undefined` when the text is not JSON.
|
|
972
|
+
*/
|
|
973
|
+
export function parsePolicyJSON(content: string): unknown {
|
|
974
|
+
try {
|
|
975
|
+
return JSON.parse(content)
|
|
976
|
+
} catch {
|
|
977
|
+
return undefined
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
/**
|
|
982
|
+
* Reads one file and parses its JSON without throwing.
|
|
983
|
+
*
|
|
984
|
+
* @remarks
|
|
985
|
+
* One boundary holds the read and the parse, so a file the host refuses to open answers as a file
|
|
986
|
+
* that holds no JSON.
|
|
987
|
+
*
|
|
988
|
+
* @param path - The absolute path of the file to read.
|
|
989
|
+
* @returns The parsed value, or `undefined` when the file cannot be read or holds no JSON.
|
|
990
|
+
*/
|
|
991
|
+
export function readPolicyJSON(path: string): unknown {
|
|
992
|
+
try {
|
|
993
|
+
return JSON.parse(readFileSync(path, 'utf8'))
|
|
994
|
+
} catch {
|
|
995
|
+
return undefined
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
/**
|
|
1000
|
+
* Extracts the lines beneath one heading, up to the next second-level heading.
|
|
1001
|
+
*
|
|
1002
|
+
* @param lines - The document lines.
|
|
1003
|
+
* @param heading - The index of the heading line whose section is read.
|
|
1004
|
+
* @returns Each line after the heading and before the next line starting with `## `, in order.
|
|
1005
|
+
*/
|
|
1006
|
+
export function extractPolicySection(lines: readonly string[], heading: number): readonly string[] {
|
|
1007
|
+
const following = lines.slice(heading + 1)
|
|
1008
|
+
const end = following.findIndex((line) => line.startsWith('## '))
|
|
1009
|
+
return end === -1 ? following : following.slice(0, end)
|
|
1010
|
+
}
|
|
1011
|
+
|
|
439
1012
|
/**
|
|
440
1013
|
* Creates one stable violation for an inspection result.
|
|
441
1014
|
*
|
|
@@ -477,12 +1050,11 @@ export function stemToPolicyCandidates(stem: string): readonly string[] {
|
|
|
477
1050
|
const normalized = normalizePolicyPath(stem)
|
|
478
1051
|
const directory = dirname(normalized).replaceAll('\\', '/')
|
|
479
1052
|
const name = basename(normalized)
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
return candidates
|
|
1053
|
+
return [
|
|
1054
|
+
...POLICY_MODULE_EXTENSIONS.map((extension) => `${normalized}.${extension}`),
|
|
1055
|
+
...POLICY_PARTIAL_EXTENSIONS.map((extension) => `${directory}/_${name}.${extension}`),
|
|
1056
|
+
...(name.startsWith(POLICY_TESTS_MODULE_PREFIX) ? [`tests/${normalized}.ts`] : []),
|
|
1057
|
+
]
|
|
486
1058
|
}
|
|
487
1059
|
|
|
488
1060
|
/**
|
|
@@ -496,23 +1068,25 @@ export function inspectPolicyMirrorPaths(
|
|
|
496
1068
|
tests: readonly string[],
|
|
497
1069
|
modules: ReadonlySet<string>,
|
|
498
1070
|
): readonly PolicyViolation[] {
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
1071
|
+
// The mapper runs on each entry as the iterator yields it, so a hole reaches normalization as
|
|
1072
|
+
// `undefined` and no entry after a failing one is read, as in the loop this replaced.
|
|
1073
|
+
return Iterator.from(tests)
|
|
1074
|
+
.flatMap((test) => {
|
|
1075
|
+
const path = normalizePolicyPath(test)
|
|
1076
|
+
const stem = testToPolicyStem(path)
|
|
1077
|
+
if (stem === undefined) return []
|
|
1078
|
+
const candidates = stemToPolicyCandidates(stem)
|
|
1079
|
+
return candidates.some((candidate) => modules.has(candidate))
|
|
1080
|
+
? []
|
|
1081
|
+
: [
|
|
1082
|
+
createPolicyViolation(
|
|
1083
|
+
'mirror',
|
|
1084
|
+
path,
|
|
1085
|
+
`module test requires one matching module: ${candidates.join(', ')}`,
|
|
1086
|
+
),
|
|
1087
|
+
]
|
|
1088
|
+
})
|
|
1089
|
+
.toArray()
|
|
516
1090
|
}
|
|
517
1091
|
|
|
518
1092
|
/**
|
|
@@ -541,15 +1115,15 @@ export function inspectPolicySetup(root: string): readonly PolicyViolation[] {
|
|
|
541
1115
|
const shared = paths.has('tests/setup.test.ts')
|
|
542
1116
|
? readFileSync(join(root, 'tests/setup.test.ts'), 'utf8')
|
|
543
1117
|
: ''
|
|
544
|
-
const imports = new Set
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
1118
|
+
const imports = new Set(
|
|
1119
|
+
parseAst(shared, { lang: 'ts' }).body.flatMap((declaration) =>
|
|
1120
|
+
declaration.type === 'ImportDeclaration' &&
|
|
1121
|
+
/^\.\/setup[^/]*\.(?:js|ts)$/u.test(declaration.source.value)
|
|
1122
|
+
? ['tests/' + declaration.source.value.slice(2).replace(/\.js$/u, '.ts')]
|
|
1123
|
+
: [],
|
|
1124
|
+
),
|
|
1125
|
+
)
|
|
1126
|
+
return [...paths].sort().flatMap((path) => {
|
|
553
1127
|
const module = path.endsWith('.test.ts') ? path.replace(/\.test\.ts$/u, '.ts') : path
|
|
554
1128
|
if (
|
|
555
1129
|
HOST_PATHS.some(
|
|
@@ -558,27 +1132,24 @@ export function inspectPolicySetup(root: string): readonly PolicyViolation[] {
|
|
|
558
1132
|
module.startsWith(normalizePolicyPath(vendored) + '/'),
|
|
559
1133
|
)
|
|
560
1134
|
)
|
|
561
|
-
|
|
1135
|
+
return []
|
|
562
1136
|
if (path.endsWith('.test.ts')) {
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
/^export /m.test(readFileSync(join(root, path), 'utf8')) &&
|
|
1137
|
+
return paths.has(module)
|
|
1138
|
+
? []
|
|
1139
|
+
: [createPolicyViolation('mirror', path, `setup proof requires its module: ${module}`)]
|
|
1140
|
+
}
|
|
1141
|
+
return /^export /m.test(readFileSync(join(root, path), 'utf8')) &&
|
|
569
1142
|
!paths.has(path.replace(/\.ts$/u, '.test.ts')) &&
|
|
570
1143
|
!imports.has(path)
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
}
|
|
581
|
-
return violations
|
|
1144
|
+
? [
|
|
1145
|
+
createPolicyViolation(
|
|
1146
|
+
'mirror',
|
|
1147
|
+
path,
|
|
1148
|
+
'exporting setup module requires its sibling proof or an import from tests/setup.test.ts',
|
|
1149
|
+
),
|
|
1150
|
+
]
|
|
1151
|
+
: []
|
|
1152
|
+
})
|
|
582
1153
|
}
|
|
583
1154
|
|
|
584
1155
|
/**
|
|
@@ -588,25 +1159,23 @@ export function inspectPolicySetup(root: string): readonly PolicyViolation[] {
|
|
|
588
1159
|
* @returns Every suppression occurrence in path and line order.
|
|
589
1160
|
*/
|
|
590
1161
|
export function inspectPolicySuppressions(root: string): readonly PolicyViolation[] {
|
|
591
|
-
const violations: PolicyViolation[] = []
|
|
592
1162
|
const paths = globSync(POLICY_SUPPRESSION_GLOB, { cwd: root }).map(normalizePolicyPath).sort()
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
return violations
|
|
1163
|
+
return paths.flatMap((path) =>
|
|
1164
|
+
readFileSync(join(root, path), 'utf8')
|
|
1165
|
+
.split('\n')
|
|
1166
|
+
.flatMap((line, index) =>
|
|
1167
|
+
POLICY_SUPPRESSION_PATTERN.test(line)
|
|
1168
|
+
? [
|
|
1169
|
+
createPolicyViolation(
|
|
1170
|
+
'suppression',
|
|
1171
|
+
path,
|
|
1172
|
+
'file carries a lint or formatter suppression directive',
|
|
1173
|
+
index + 1,
|
|
1174
|
+
),
|
|
1175
|
+
]
|
|
1176
|
+
: [],
|
|
1177
|
+
),
|
|
1178
|
+
)
|
|
610
1179
|
}
|
|
611
1180
|
|
|
612
1181
|
/**
|
|
@@ -616,63 +1185,59 @@ export function inspectPolicySuppressions(root: string): readonly PolicyViolatio
|
|
|
616
1185
|
* @returns Every wiring violation in rule and configuration order.
|
|
617
1186
|
*/
|
|
618
1187
|
export function inspectPolicyConfiguration(configuration: unknown): readonly string[] {
|
|
619
|
-
const violations: string[] = []
|
|
620
1188
|
if (!isPolicyRecord(configuration)) {
|
|
621
1189
|
return ['Oxlint configuration must be a record']
|
|
622
1190
|
}
|
|
623
1191
|
|
|
624
1192
|
const rules: unknown = Object.getOwnPropertyDescriptor(configuration, 'rules')?.value
|
|
625
|
-
for (const rule of POLICY_WIRING_RULES) {
|
|
626
|
-
const setting = isPolicyRecord(rules)
|
|
627
|
-
? Object.getOwnPropertyDescriptor(rules, rule)?.value
|
|
628
|
-
: undefined
|
|
629
|
-
const severity = Array.isArray(setting) ? setting[0] : setting
|
|
630
|
-
if (severity !== 'error') violations.push(`${rule} must have top-level error severity`)
|
|
631
|
-
}
|
|
632
|
-
|
|
633
1193
|
const ignorePatterns: unknown = Object.getOwnPropertyDescriptor(
|
|
634
1194
|
configuration,
|
|
635
1195
|
'ignorePatterns',
|
|
636
1196
|
)?.value
|
|
637
|
-
if (ignorePatterns !== undefined && !Array.isArray(ignorePatterns)) {
|
|
638
|
-
violations.push('ignorePatterns must be an array when declared')
|
|
639
|
-
} else if (Array.isArray(ignorePatterns)) {
|
|
640
|
-
for (const pattern of ignorePatterns) {
|
|
641
|
-
if (typeof pattern !== 'string' || pattern.startsWith('!')) continue
|
|
642
|
-
const normalized = normalizePolicyPath(pattern).replace(/^\.\//u, '').replace(/^\//u, '')
|
|
643
|
-
const [first = ''] = normalized.split('/')
|
|
644
|
-
if (
|
|
645
|
-
POLICY_WIRING_ROOTS.some(
|
|
646
|
-
(root) => first === root || (first !== '' && matchesGlob(root, first)),
|
|
647
|
-
)
|
|
648
|
-
) {
|
|
649
|
-
violations.push(`ignorePatterns must not reach ${pattern}`)
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
|
|
654
1197
|
const overrides: unknown = Object.getOwnPropertyDescriptor(configuration, 'overrides')?.value
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
1198
|
+
return [
|
|
1199
|
+
...POLICY_WIRING_RULES.flatMap((rule) => {
|
|
1200
|
+
const setting = isPolicyRecord(rules)
|
|
1201
|
+
? Object.getOwnPropertyDescriptor(rules, rule)?.value
|
|
1202
|
+
: undefined
|
|
1203
|
+
const severity = Array.isArray(setting) ? setting[0] : setting
|
|
1204
|
+
return severity === 'error' ? [] : [`${rule} must have top-level error severity`]
|
|
1205
|
+
}),
|
|
1206
|
+
...(ignorePatterns !== undefined && !Array.isArray(ignorePatterns)
|
|
1207
|
+
? ['ignorePatterns must be an array when declared']
|
|
1208
|
+
: // The mapper runs on each pattern as the iterator yields it, as the loop it replaced did.
|
|
1209
|
+
Iterator.from(Array.isArray(ignorePatterns) ? ignorePatterns : [])
|
|
1210
|
+
.flatMap((pattern) => {
|
|
1211
|
+
if (typeof pattern !== 'string' || pattern.startsWith('!')) return []
|
|
1212
|
+
const normalized = normalizePolicyPath(pattern)
|
|
1213
|
+
.replace(/^\.\//u, '')
|
|
1214
|
+
.replace(/^\//u, '')
|
|
1215
|
+
const [first = ''] = normalized.split('/')
|
|
1216
|
+
return POLICY_WIRING_ROOTS.some(
|
|
1217
|
+
(root) => first === root || (first !== '' && matchesGlob(root, first)),
|
|
1218
|
+
)
|
|
1219
|
+
? [`ignorePatterns must not reach ${pattern}`]
|
|
1220
|
+
: []
|
|
1221
|
+
})
|
|
1222
|
+
.toArray()),
|
|
1223
|
+
...(overrides !== undefined && !Array.isArray(overrides)
|
|
1224
|
+
? ['overrides must be an array when declared']
|
|
1225
|
+
: // The mapper runs on each entry as the iterator yields it, so a hole is a malformed entry as it
|
|
1226
|
+
// was in the loop, and no entry after a failing one is read.
|
|
1227
|
+
Iterator.from(Array.isArray(overrides) ? overrides : [])
|
|
1228
|
+
.flatMap((override) => {
|
|
1229
|
+
if (!isPolicyRecord(override)) return ['override entries must be records']
|
|
1230
|
+
const overrideRules: unknown = Object.getOwnPropertyDescriptor(override, 'rules')?.value
|
|
1231
|
+
return isPolicyRecord(overrideRules)
|
|
1232
|
+
? POLICY_WIRING_RULES.flatMap((rule) =>
|
|
1233
|
+
Object.getOwnPropertyDescriptor(overrideRules, rule) === undefined
|
|
1234
|
+
? []
|
|
1235
|
+
: [`overrides must not configure ${rule}`],
|
|
1236
|
+
)
|
|
1237
|
+
: []
|
|
1238
|
+
})
|
|
1239
|
+
.toArray()),
|
|
1240
|
+
]
|
|
676
1241
|
}
|
|
677
1242
|
|
|
678
1243
|
/**
|
|
@@ -692,36 +1257,39 @@ export function inspectPolicyWiring(
|
|
|
692
1257
|
return ['Oxlint configuration must be a record']
|
|
693
1258
|
}
|
|
694
1259
|
|
|
695
|
-
const
|
|
696
|
-
const declaredPopulations: string[] = []
|
|
1260
|
+
const topRules: unknown = Object.getOwnPropertyDescriptor(configuration, 'rules')?.value
|
|
697
1261
|
const overrides: unknown = Object.getOwnPropertyDescriptor(configuration, 'overrides')?.value
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
1262
|
+
// Each mapper runs on an entry as the iterator yields it, so a hole reaches the guard as
|
|
1263
|
+
// `undefined` and no entry after a failing one is read, as in the loops these replaced.
|
|
1264
|
+
const entries = Iterator.from(Array.isArray(overrides) ? overrides : [])
|
|
1265
|
+
.flatMap((entry) => {
|
|
1266
|
+
if (!isPolicyRecord(entry)) return []
|
|
1267
|
+
const record: unknown = Object.getOwnPropertyDescriptor(entry, 'rules')?.value
|
|
702
1268
|
const files: unknown = Object.getOwnPropertyDescriptor(entry, 'files')?.value
|
|
703
|
-
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
1269
|
+
return [{ record, declared: Array.isArray(files) ? [files.join(' ')] : [] }]
|
|
1270
|
+
})
|
|
1271
|
+
.toArray()
|
|
1272
|
+
const enabled = new Set(
|
|
1273
|
+
[topRules, ...entries.map(({ record }) => record)].flatMap((record) =>
|
|
1274
|
+
isPolicyRecord(record) ? Object.getOwnPropertyNames(record) : [],
|
|
1275
|
+
),
|
|
1276
|
+
)
|
|
1277
|
+
const declaredPopulations = entries.flatMap(({ declared }) => declared)
|
|
1278
|
+
return [
|
|
1279
|
+
...Iterator.from(rules)
|
|
1280
|
+
.flatMap((rule) =>
|
|
1281
|
+
enabled.has(rule) ? [] : [`${rule} is enabled by no top-level or override rules record`],
|
|
1282
|
+
)
|
|
1283
|
+
.toArray(),
|
|
1284
|
+
...Iterator.from(populations)
|
|
1285
|
+
.flatMap((population) => {
|
|
1286
|
+
const joined = population.join(' ')
|
|
1287
|
+
return declaredPopulations.includes(joined)
|
|
1288
|
+
? []
|
|
1289
|
+
: [`no override files list declares the population exactly: ${joined}`]
|
|
1290
|
+
})
|
|
1291
|
+
.toArray(),
|
|
1292
|
+
]
|
|
725
1293
|
}
|
|
726
1294
|
|
|
727
1295
|
/**
|
|
@@ -734,15 +1302,13 @@ export function inspectPolicyWiring(
|
|
|
734
1302
|
export function resolvePolicyDirectory(root: string, path: string): string | undefined {
|
|
735
1303
|
const normalized = normalizePolicyPath(path)
|
|
736
1304
|
if (normalized === '' || normalized === '.') return root
|
|
737
|
-
|
|
738
|
-
|
|
1305
|
+
return normalized.split('/').reduce<string | undefined>((current, segment) => {
|
|
1306
|
+
if (current === undefined) return undefined
|
|
739
1307
|
const entry = readdirSync(current, { withFileTypes: true }).find(
|
|
740
1308
|
(candidate) => candidate.name === segment && candidate.isDirectory(),
|
|
741
1309
|
)
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
}
|
|
745
|
-
return current
|
|
1310
|
+
return entry === undefined ? undefined : join(current, entry.name)
|
|
1311
|
+
}, root)
|
|
746
1312
|
}
|
|
747
1313
|
|
|
748
1314
|
/**
|
|
@@ -800,76 +1366,68 @@ export function parseSkillFrontmatter(content: string): SkillFrontmatter | undef
|
|
|
800
1366
|
if (lines[0] !== '---') return undefined
|
|
801
1367
|
const boundary = lines.indexOf('---', 1)
|
|
802
1368
|
if (boundary === -1) return undefined
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
const
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
value
|
|
853
|
-
blanks = 0
|
|
1369
|
+
// A folded description consumes the lines after its key, so `skipThrough` carries the last of them.
|
|
1370
|
+
const scan = lines.slice(0, boundary).reduce<SkillFrontmatterScan | undefined>(
|
|
1371
|
+
(state, line, index) => {
|
|
1372
|
+
if (state === undefined || index === 0 || index <= state.skipThrough) return state
|
|
1373
|
+
const match = line.match(/^([A-Za-z][A-Za-z0-9_-]*):(.*)$/u)
|
|
1374
|
+
const key = match?.[1]
|
|
1375
|
+
const scalar = match?.[2]
|
|
1376
|
+
if (key === undefined || scalar === undefined) return undefined
|
|
1377
|
+
if (scalar !== '' && !scalar.startsWith(' ')) return undefined
|
|
1378
|
+
const initial = scalar === '' ? '' : scalar.slice(1)
|
|
1379
|
+
const own = rawLines[index]?.slice(line.indexOf(':') + 1)
|
|
1380
|
+
if (own === undefined) return undefined
|
|
1381
|
+
if (initial === '>-' && key !== 'description') return undefined
|
|
1382
|
+
if (
|
|
1383
|
+
initial !== '>-' &&
|
|
1384
|
+
key === 'description' &&
|
|
1385
|
+
(/^['"]/u.test(initial) || /^[>|][+-]?$/u.test(initial))
|
|
1386
|
+
)
|
|
1387
|
+
return undefined
|
|
1388
|
+
const rest = initial === '>-' ? lines.slice(index + 1, boundary) : []
|
|
1389
|
+
const ended = rest.findIndex(
|
|
1390
|
+
(continuation) => continuation.trim() !== '' && !continuation.startsWith(' '),
|
|
1391
|
+
)
|
|
1392
|
+
const span = ended === -1 ? rest.length : ended
|
|
1393
|
+
const block = rest.slice(0, span)
|
|
1394
|
+
const rawBlock = rawLines.slice(index + 1, index + 1 + span)
|
|
1395
|
+
if (rawBlock.length !== span) return undefined
|
|
1396
|
+
const folded = block.map((continuation) =>
|
|
1397
|
+
continuation.trim() === '' ? '' : continuation.slice(2),
|
|
1398
|
+
)
|
|
1399
|
+
// Each kept line joins to the one before it by a space, or by one newline per blank line between.
|
|
1400
|
+
const kept = folded.flatMap((continuation, position) =>
|
|
1401
|
+
continuation === '' ? [] : [{ continuation, position }],
|
|
1402
|
+
)
|
|
1403
|
+
const value =
|
|
1404
|
+
initial === '>-'
|
|
1405
|
+
? kept
|
|
1406
|
+
.map((entry, rank) => {
|
|
1407
|
+
const before = kept[rank - 1]
|
|
1408
|
+
if (before === undefined) return entry.continuation
|
|
1409
|
+
const blanks = entry.position - before.position - 1
|
|
1410
|
+
return `${blanks === 0 ? ' ' : '\n'.repeat(blanks)}${entry.continuation}`
|
|
1411
|
+
})
|
|
1412
|
+
.join('')
|
|
1413
|
+
: initial
|
|
1414
|
+
const source = initial === '>-' ? [own, ...rawBlock].join('\n') : own
|
|
1415
|
+
return {
|
|
1416
|
+
keys: [...state.keys, key],
|
|
1417
|
+
skipThrough: index + span,
|
|
1418
|
+
fields: [...state.fields, [key, { value, source }]],
|
|
854
1419
|
}
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
name = value
|
|
861
|
-
nameSource = source
|
|
862
|
-
} else if (key === 'description') {
|
|
863
|
-
description = value
|
|
864
|
-
descriptionSource = source
|
|
865
|
-
}
|
|
866
|
-
}
|
|
1420
|
+
},
|
|
1421
|
+
{ keys: [], skipThrough: 0, fields: [] },
|
|
1422
|
+
)
|
|
1423
|
+
if (scan === undefined) return undefined
|
|
1424
|
+
const fields = new Map(scan.fields)
|
|
867
1425
|
|
|
868
1426
|
return {
|
|
869
|
-
keys,
|
|
870
|
-
name,
|
|
871
|
-
description,
|
|
872
|
-
source: { name:
|
|
1427
|
+
keys: scan.keys,
|
|
1428
|
+
name: fields.get('name')?.value,
|
|
1429
|
+
description: fields.get('description')?.value,
|
|
1430
|
+
source: { name: fields.get('name')?.source, description: fields.get('description')?.source },
|
|
873
1431
|
}
|
|
874
1432
|
}
|
|
875
1433
|
|
|
@@ -928,16 +1486,16 @@ export function readSkillScripts(root: string, name: string): readonly string[]
|
|
|
928
1486
|
* @returns The distinct script paths, relative to the skill directory, in first-mention order.
|
|
929
1487
|
*/
|
|
930
1488
|
export function extractSkillScripts(content: string, name: string): readonly string[] {
|
|
931
|
-
const
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
1489
|
+
const named = [
|
|
1490
|
+
...content.matchAll(
|
|
1491
|
+
/(?:\.agents\/skills\/([A-Za-z0-9-]+)\/)?(scripts\/[A-Za-z0-9][A-Za-z0-9._-]*\.ts)/gu,
|
|
1492
|
+
),
|
|
1493
|
+
].flatMap((match) => {
|
|
935
1494
|
const owner = match[1]
|
|
936
1495
|
const script = match[2]
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
return found
|
|
1496
|
+
return script === undefined || (owner !== undefined && owner !== name) ? [] : [script]
|
|
1497
|
+
})
|
|
1498
|
+
return named.filter((script, index) => named.indexOf(script) === index)
|
|
941
1499
|
}
|
|
942
1500
|
|
|
943
1501
|
/**
|
|
@@ -948,14 +1506,14 @@ export function extractSkillScripts(content: string, name: string): readonly str
|
|
|
948
1506
|
* @returns The distinct `.agents/skills/<other>/scripts/*.ts` paths, in first-mention order.
|
|
949
1507
|
*/
|
|
950
1508
|
export function extractForeignSkillScripts(content: string, name: string): readonly string[] {
|
|
951
|
-
const
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
return
|
|
1509
|
+
const named = [
|
|
1510
|
+
...content.matchAll(
|
|
1511
|
+
/\.agents\/skills\/([A-Za-z0-9-]+)\/scripts\/[A-Za-z0-9][A-Za-z0-9._-]*\.ts/gu,
|
|
1512
|
+
),
|
|
1513
|
+
]
|
|
1514
|
+
.filter((match) => match[1] !== name)
|
|
1515
|
+
.map((match) => match[0])
|
|
1516
|
+
return named.filter((script, index) => named.indexOf(script) === index)
|
|
959
1517
|
}
|
|
960
1518
|
|
|
961
1519
|
/**
|
|
@@ -1008,11 +1566,14 @@ export function parseSkillPrompt(content: string): string | undefined {
|
|
|
1008
1566
|
*/
|
|
1009
1567
|
export function matchesSkillToken(prompt: string, name: string): boolean {
|
|
1010
1568
|
const token = `$${name}`
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1569
|
+
// The token opens with `$`, so each `$` in the prompt is a start to try, and a name that carries
|
|
1570
|
+
// `$` makes the token overlap itself.
|
|
1571
|
+
return prompt
|
|
1572
|
+
.matchAll(/\$/gu)
|
|
1573
|
+
.some(
|
|
1574
|
+
({ index }) =>
|
|
1575
|
+
prompt.startsWith(token, index) && !/[a-z-]/u.test(prompt.charAt(index + token.length)),
|
|
1576
|
+
)
|
|
1016
1577
|
}
|
|
1017
1578
|
|
|
1018
1579
|
/**
|
|
@@ -1022,12 +1583,12 @@ export function matchesSkillToken(prompt: string, name: string): boolean {
|
|
|
1022
1583
|
* @returns Each distinct references/name.md token in sorted order.
|
|
1023
1584
|
*/
|
|
1024
1585
|
export function extractSkillReferences(content: string): readonly string[] {
|
|
1025
|
-
const references = new Set<string>()
|
|
1026
1586
|
// This raw-text scan includes fenced examples. Over-matching safely requires the named file.
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1587
|
+
const references = new Set(
|
|
1588
|
+
[...content.matchAll(/references\/[A-Za-z0-9._-]+\.md/gu)].flatMap((match) =>
|
|
1589
|
+
match[0] === undefined ? [] : [match[0]],
|
|
1590
|
+
),
|
|
1591
|
+
)
|
|
1031
1592
|
return [...references].sort()
|
|
1032
1593
|
}
|
|
1033
1594
|
|
|
@@ -1052,41 +1613,32 @@ export function inspectSkillTemplateTODOs(
|
|
|
1052
1613
|
path: string,
|
|
1053
1614
|
content: string,
|
|
1054
1615
|
): readonly PolicyViolation[] {
|
|
1055
|
-
const violations: PolicyViolation[] = []
|
|
1056
1616
|
const lines = content.split(/\r\n|\r|\n/u)
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
fence = opening
|
|
1069
|
-
continue
|
|
1070
|
-
}
|
|
1071
|
-
let cursor = 0
|
|
1072
|
-
while (cursor < line.length) {
|
|
1073
|
-
const openingBacktick = line.indexOf('`', cursor)
|
|
1074
|
-
const end = openingBacktick === -1 ? line.length : openingBacktick
|
|
1075
|
-
for (
|
|
1076
|
-
let todo = line.indexOf('TODO', cursor);
|
|
1077
|
-
todo !== -1 && todo < end;
|
|
1078
|
-
todo = line.indexOf('TODO', todo + 4)
|
|
1079
|
-
) {
|
|
1080
|
-
violations.push(
|
|
1081
|
-
createPolicyViolation(rule, path, 'skill documents contain no template TODOs', index + 1),
|
|
1082
|
-
)
|
|
1617
|
+
// Each line the scan reads is one outside every fence; the fence open at a line rides the fold.
|
|
1618
|
+
const scanned = lines.reduce<{
|
|
1619
|
+
readonly fence: string | undefined
|
|
1620
|
+
readonly indexes: readonly number[]
|
|
1621
|
+
}>(
|
|
1622
|
+
(state, line, index) => {
|
|
1623
|
+
const fenceLine = line.replace(/^ {0,3}/u, '')
|
|
1624
|
+
if (state.fence !== undefined) {
|
|
1625
|
+
return fenceLine.startsWith(state.fence)
|
|
1626
|
+
? { fence: undefined, indexes: state.indexes }
|
|
1627
|
+
: state
|
|
1083
1628
|
}
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
}
|
|
1088
|
-
|
|
1089
|
-
|
|
1629
|
+
const opening = fenceLine.match(/^(`{3,}|~{3,})/u)?.[1]
|
|
1630
|
+
if (opening !== undefined) return { fence: opening, indexes: state.indexes }
|
|
1631
|
+
return { fence: undefined, indexes: [...state.indexes, index] }
|
|
1632
|
+
},
|
|
1633
|
+
{ fence: undefined, indexes: [] },
|
|
1634
|
+
).indexes
|
|
1635
|
+
return scanned.flatMap((index) => {
|
|
1636
|
+
// A matched pair of single backticks on one line is an inline span; a lone backtick is not.
|
|
1637
|
+
const outside = (lines[index] ?? '').replace(/`[^`]*`/gu, ' ')
|
|
1638
|
+
return Array.from({ length: outside.split('TODO').length - 1 }, () =>
|
|
1639
|
+
createPolicyViolation(rule, path, 'skill documents contain no template TODOs', index + 1),
|
|
1640
|
+
)
|
|
1641
|
+
})
|
|
1090
1642
|
}
|
|
1091
1643
|
|
|
1092
1644
|
/** Names why a declaration reading produced no inventory. */
|
|
@@ -1335,8 +1887,8 @@ export function readSkillDeclarations(
|
|
|
1335
1887
|
const source = parseSync(path, readFileSync(path, 'utf8'))
|
|
1336
1888
|
const [failure] = source.errors
|
|
1337
1889
|
if (failure !== undefined) return { outcome: 'syntax', names: [], detail: failure.message }
|
|
1338
|
-
|
|
1339
|
-
|
|
1890
|
+
// Each statement answers its own declared names or the refusal it raises; the first refusal wins.
|
|
1891
|
+
const declared = source.program.body.map((statement): SkillDeclarationResult => {
|
|
1340
1892
|
if (statement.type === 'ExportDefaultDeclaration')
|
|
1341
1893
|
return { outcome: 'form', names: [], detail: 'export default' }
|
|
1342
1894
|
if (statement.type === 'TSExportAssignment')
|
|
@@ -1345,69 +1897,90 @@ export function readSkillDeclarations(
|
|
|
1345
1897
|
return { outcome: 'form', names: [], detail: 'export as namespace' }
|
|
1346
1898
|
if (statement.type === 'TSModuleDeclaration')
|
|
1347
1899
|
return { outcome: 'form', names: [], detail: 'ambient module' }
|
|
1348
|
-
if (statement.type !== 'ExportNamedDeclaration' || statement.source !== null)
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1900
|
+
if (statement.type !== 'ExportNamedDeclaration' || statement.source !== null)
|
|
1901
|
+
return { outcome: 'read', names: [] }
|
|
1902
|
+
const listed = statement.specifiers.map((binding) =>
|
|
1903
|
+
binding.exported.type === 'Identifier' ? binding.exported.name : binding.exported.value,
|
|
1904
|
+
)
|
|
1905
|
+
if (listed.includes('default'))
|
|
1906
|
+
return { outcome: 'form', names: [], detail: 'default export list' }
|
|
1355
1907
|
const declaration = statement.declaration
|
|
1356
|
-
if (declaration === null)
|
|
1908
|
+
if (declaration === null) return { outcome: 'read', names: listed }
|
|
1357
1909
|
if (declaration.type === 'VariableDeclaration') {
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1910
|
+
const identifiers = declaration.declarations.flatMap((variable) =>
|
|
1911
|
+
variable.id.type === 'Identifier' ? [variable.id.name] : [],
|
|
1912
|
+
)
|
|
1913
|
+
return identifiers.length === declaration.declarations.length
|
|
1914
|
+
? { outcome: 'read', names: [...listed, ...identifiers] }
|
|
1915
|
+
: { outcome: 'form', names: [], detail: 'destructured declaration' }
|
|
1916
|
+
}
|
|
1917
|
+
if (declaration.type === 'TSModuleDeclaration')
|
|
1364
1918
|
return { outcome: 'form', names: [], detail: 'exported namespace' }
|
|
1365
|
-
|
|
1919
|
+
if (
|
|
1366
1920
|
declaration.type === 'TSDeclareFunction' ||
|
|
1367
1921
|
declaration.type === 'ClassDeclaration' ||
|
|
1368
1922
|
declaration.type === 'TSEnumDeclaration' ||
|
|
1369
1923
|
declaration.type === 'TSInterfaceDeclaration' ||
|
|
1370
1924
|
declaration.type === 'TSTypeAliasDeclaration'
|
|
1371
1925
|
) {
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
} else return { outcome: 'form', names: [], detail: 'unsupported exported declaration' }
|
|
1376
|
-
}
|
|
1377
|
-
if (ancestors.includes(path)) return { outcome: 'cycle', names: [...names] }
|
|
1378
|
-
for (const statement of source.program.body) {
|
|
1379
|
-
if (statement.type !== 'ExportAllDeclaration' && statement.type !== 'ExportNamedDeclaration')
|
|
1380
|
-
continue
|
|
1381
|
-
const origin = statement.source
|
|
1382
|
-
if (origin === null) continue
|
|
1383
|
-
if (statement.type === 'ExportAllDeclaration' && statement.exported !== null)
|
|
1384
|
-
return { outcome: 'form', names: [], detail: 'star export alias' }
|
|
1385
|
-
if (!origin.value.startsWith('./') && !origin.value.startsWith('../'))
|
|
1386
|
-
return { outcome: 'form', names: [], detail: 'non-relative re-export' }
|
|
1387
|
-
const declaration = /\.d\.(?:ts|mts|cts)$/u.test(origin.value)
|
|
1388
|
-
? origin.value
|
|
1389
|
-
: origin.value.replace(/\.(js|mjs|cjs)$/u, (_, extension: string) =>
|
|
1390
|
-
extension === 'mjs' ? '.d.mts' : extension === 'cjs' ? '.d.cts' : '.d.ts',
|
|
1391
|
-
)
|
|
1392
|
-
if (!/\.d\.(?:ts|mts|cts)$/u.test(declaration))
|
|
1393
|
-
return { outcome: 'form', names: [], detail: 'untyped re-export target' }
|
|
1394
|
-
const targets = readSkillDeclarations(resolve(dirname(path), declaration), [...ancestors, path])
|
|
1395
|
-
if (targets.outcome !== 'read' && targets.outcome !== 'cycle') return targets
|
|
1396
|
-
if (statement.type === 'ExportAllDeclaration') {
|
|
1397
|
-
if (targets.outcome === 'read') for (const name of targets.names) names.add(name)
|
|
1398
|
-
continue
|
|
1926
|
+
return declaration.id === null
|
|
1927
|
+
? { outcome: 'form', names: [], detail: 'anonymous declaration' }
|
|
1928
|
+
: { outcome: 'read', names: [...listed, declaration.id.name] }
|
|
1399
1929
|
}
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1930
|
+
return { outcome: 'form', names: [], detail: 'unsupported exported declaration' }
|
|
1931
|
+
})
|
|
1932
|
+
const refused = declared.find((result) => result.outcome !== 'read')
|
|
1933
|
+
if (refused !== undefined) return refused
|
|
1934
|
+
const names = [...new Set(declared.flatMap((result) => result.names))]
|
|
1935
|
+
if (ancestors.includes(path)) return { outcome: 'cycle', names }
|
|
1936
|
+
// A refusal short-circuits the fold, so no later re-export is read.
|
|
1937
|
+
const reexported = source.program.body.reduce<SkillDeclarationResult>(
|
|
1938
|
+
(state, statement) => {
|
|
1939
|
+
if (state.outcome !== 'read') return state
|
|
1940
|
+
if (statement.type !== 'ExportAllDeclaration' && statement.type !== 'ExportNamedDeclaration')
|
|
1941
|
+
return state
|
|
1942
|
+
const origin = statement.source
|
|
1943
|
+
if (origin === null) return state
|
|
1944
|
+
if (statement.type === 'ExportAllDeclaration' && statement.exported !== null)
|
|
1945
|
+
return { outcome: 'form', names: [], detail: 'star export alias' }
|
|
1946
|
+
if (!origin.value.startsWith('./') && !origin.value.startsWith('../'))
|
|
1947
|
+
return { outcome: 'form', names: [], detail: 'non-relative re-export' }
|
|
1948
|
+
const declaration = /\.d\.(?:ts|mts|cts)$/u.test(origin.value)
|
|
1949
|
+
? origin.value
|
|
1950
|
+
: origin.value.replace(/\.(js|mjs|cjs)$/u, (_, extension: string) =>
|
|
1951
|
+
extension === 'mjs' ? '.d.mts' : extension === 'cjs' ? '.d.cts' : '.d.ts',
|
|
1952
|
+
)
|
|
1953
|
+
if (!/\.d\.(?:ts|mts|cts)$/u.test(declaration))
|
|
1954
|
+
return { outcome: 'form', names: [], detail: 'untyped re-export target' }
|
|
1955
|
+
const targets = readSkillDeclarations(resolve(dirname(path), declaration), [
|
|
1956
|
+
...ancestors,
|
|
1957
|
+
path,
|
|
1958
|
+
])
|
|
1959
|
+
if (targets.outcome !== 'read' && targets.outcome !== 'cycle') return targets
|
|
1960
|
+
if (statement.type === 'ExportAllDeclaration')
|
|
1961
|
+
return {
|
|
1962
|
+
outcome: 'read',
|
|
1963
|
+
names: [...state.names, ...(targets.outcome === 'read' ? targets.names : [])],
|
|
1964
|
+
}
|
|
1965
|
+
const bindings = statement.specifiers.map((binding) => ({
|
|
1966
|
+
name:
|
|
1967
|
+
binding.exported.type === 'Identifier' ? binding.exported.name : binding.exported.value,
|
|
1968
|
+
original: binding.local.type === 'Identifier' ? binding.local.name : binding.local.value,
|
|
1969
|
+
}))
|
|
1970
|
+
const rejected = bindings.find(
|
|
1971
|
+
({ name, original }) => name === 'default' || !targets.names.includes(original),
|
|
1972
|
+
)
|
|
1973
|
+
if (rejected === undefined)
|
|
1974
|
+
return { outcome: 'read', names: [...state.names, ...bindings.map(({ name }) => name)] }
|
|
1975
|
+
return rejected.name === 'default'
|
|
1976
|
+
? { outcome: 'form', names: [], detail: 'default re-export' }
|
|
1977
|
+
: { outcome: 'name', names: [], detail: rejected.original }
|
|
1978
|
+
},
|
|
1979
|
+
{ outcome: 'read', names },
|
|
1980
|
+
)
|
|
1981
|
+
return reexported.outcome === 'read'
|
|
1982
|
+
? { outcome: 'read', names: [...new Set(reexported.names)].sort() }
|
|
1983
|
+
: reexported
|
|
1411
1984
|
}
|
|
1412
1985
|
|
|
1413
1986
|
/**
|
|
@@ -1434,63 +2007,84 @@ export function inspectSkillImports(
|
|
|
1434
2007
|
path: string,
|
|
1435
2008
|
content: string,
|
|
1436
2009
|
): readonly PolicyViolation[] {
|
|
1437
|
-
const
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
2010
|
+
const fences = createGuide(content)
|
|
2011
|
+
.fences()
|
|
2012
|
+
.map((fence) => {
|
|
2013
|
+
const source = parseSync('skill.ts', fence.code)
|
|
2014
|
+
const [failure] = source.errors
|
|
2015
|
+
const refused = failure !== undefined && fence.code.includes('@orkestrel/')
|
|
2016
|
+
return {
|
|
2017
|
+
failure: refused ? failure : undefined,
|
|
2018
|
+
imports: refused
|
|
2019
|
+
? []
|
|
2020
|
+
: source.program.body.flatMap((statement) => {
|
|
2021
|
+
if (statement.type !== 'ImportDeclaration') return []
|
|
2022
|
+
const specifier = statement.source.value
|
|
2023
|
+
const bindings = statement.specifiers.filter(
|
|
2024
|
+
(binding) => binding.type === 'ImportSpecifier',
|
|
2025
|
+
)
|
|
2026
|
+
return specifier.startsWith('@orkestrel/') && bindings.length > 0
|
|
2027
|
+
? [{ specifier, bindings }]
|
|
2028
|
+
: []
|
|
2029
|
+
}),
|
|
2030
|
+
}
|
|
2031
|
+
})
|
|
2032
|
+
const specifiers = [
|
|
2033
|
+
...new Set(
|
|
2034
|
+
fences
|
|
2035
|
+
.flatMap(({ imports }) => imports.map(({ specifier }) => specifier))
|
|
2036
|
+
.filter((specifier) =>
|
|
2037
|
+
Object.hasOwn(BASE_DEV_DEPENDENCIES, specifier.split('/').slice(0, 2).join('/')),
|
|
2038
|
+
),
|
|
2039
|
+
),
|
|
2040
|
+
]
|
|
2041
|
+
// Each distinct supported specifier is read once, in first-seen order.
|
|
2042
|
+
const entries = new Map(
|
|
2043
|
+
specifiers.map((specifier) => [specifier, readSkillExports(root, specifier)]),
|
|
2044
|
+
)
|
|
2045
|
+
return fences.flatMap(({ failure, imports }) => {
|
|
2046
|
+
if (failure !== undefined) {
|
|
2047
|
+
return [
|
|
1444
2048
|
createPolicyViolation('skill', path, `skill fence could not be parsed: ${failure.message}`),
|
|
1445
|
-
|
|
1446
|
-
continue
|
|
2049
|
+
]
|
|
1447
2050
|
}
|
|
1448
|
-
|
|
1449
|
-
if (statement.type !== 'ImportDeclaration') continue
|
|
1450
|
-
const specifier = statement.source.value
|
|
1451
|
-
const bindings = statement.specifiers.filter((binding) => binding.type === 'ImportSpecifier')
|
|
1452
|
-
if (!specifier.startsWith('@orkestrel/') || bindings.length === 0) continue
|
|
2051
|
+
return imports.flatMap(({ specifier, bindings }) => {
|
|
1453
2052
|
const packageName = specifier.split('/').slice(0, 2).join('/')
|
|
1454
2053
|
if (!Object.hasOwn(BASE_DEV_DEPENDENCIES, packageName)) {
|
|
1455
|
-
|
|
2054
|
+
return [
|
|
1456
2055
|
createPolicyViolation(
|
|
1457
2056
|
'skill',
|
|
1458
2057
|
path,
|
|
1459
2058
|
`skill fence import ${specifier} is outside BASE_DEV_DEPENDENCIES: ${packageName}`,
|
|
1460
2059
|
),
|
|
1461
|
-
|
|
1462
|
-
continue
|
|
2060
|
+
]
|
|
1463
2061
|
}
|
|
1464
|
-
const
|
|
1465
|
-
const entry = cached ?? readSkillExports(root, specifier)
|
|
1466
|
-
if (cached === undefined) entries.set(specifier, entry)
|
|
2062
|
+
const entry = entries.get(specifier) ?? readSkillExports(root, specifier)
|
|
1467
2063
|
if (entry.outcome !== 'read' && entry.outcome !== 'cycle') {
|
|
1468
2064
|
const phrase = SKILL_DECLARATION_MESSAGES[entry.outcome]
|
|
1469
|
-
|
|
2065
|
+
return [
|
|
1470
2066
|
createPolicyViolation(
|
|
1471
2067
|
'skill',
|
|
1472
2068
|
path,
|
|
1473
2069
|
`skill fence import ${specifier} ${entry.detail === undefined ? phrase : `${phrase} ${entry.detail}`}`,
|
|
1474
2070
|
),
|
|
1475
|
-
|
|
1476
|
-
continue
|
|
2071
|
+
]
|
|
1477
2072
|
}
|
|
1478
|
-
|
|
2073
|
+
return bindings.flatMap((binding) => {
|
|
1479
2074
|
const name =
|
|
1480
2075
|
binding.imported.type === 'Identifier' ? binding.imported.name : binding.imported.value
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
}
|
|
1491
|
-
}
|
|
1492
|
-
}
|
|
1493
|
-
return violations
|
|
2076
|
+
return entry.names.includes(name)
|
|
2077
|
+
? []
|
|
2078
|
+
: [
|
|
2079
|
+
createPolicyViolation(
|
|
2080
|
+
'skill',
|
|
2081
|
+
path,
|
|
2082
|
+
`skill fence import ${specifier} does not export ${name}`,
|
|
2083
|
+
),
|
|
2084
|
+
]
|
|
2085
|
+
})
|
|
2086
|
+
})
|
|
2087
|
+
})
|
|
1494
2088
|
}
|
|
1495
2089
|
|
|
1496
2090
|
/**
|
|
@@ -1509,228 +2103,231 @@ export function inspectSkill(
|
|
|
1509
2103
|
const base = `${SKILL_FAMILY_ROOT}/${name}`
|
|
1510
2104
|
const skill = `${base}/SKILL.md`
|
|
1511
2105
|
const metadata = `${base}/agents/openai.yaml`
|
|
1512
|
-
const violations: PolicyViolation[] = []
|
|
1513
|
-
let content: string | undefined
|
|
1514
2106
|
const hasSkill = isPolicyFile(root, skill)
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
}
|
|
2107
|
+
const content = hasSkill ? readFileSync(join(root, skill), 'utf8') : undefined
|
|
2108
|
+
const frontmatter = content === undefined ? undefined : parseSkillFrontmatter(content)
|
|
2109
|
+
const keys = new Set(frontmatter?.keys)
|
|
2110
|
+
const documented =
|
|
2111
|
+
content === undefined
|
|
2112
|
+
? [createPolicyViolation('skill', skill, 'skill requires an exact-case regular SKILL.md')]
|
|
2113
|
+
: [
|
|
2114
|
+
...(frontmatter === undefined
|
|
2115
|
+
? [createPolicyViolation('skill', skill, 'SKILL.md frontmatter exists and parses')]
|
|
2116
|
+
: [
|
|
2117
|
+
...(frontmatter.keys.length !== 2 ||
|
|
2118
|
+
keys.size !== 2 ||
|
|
2119
|
+
!keys.has('name') ||
|
|
2120
|
+
!keys.has('description')
|
|
2121
|
+
? [
|
|
2122
|
+
createPolicyViolation(
|
|
2123
|
+
'skill',
|
|
2124
|
+
skill,
|
|
2125
|
+
'SKILL.md frontmatter contains exactly name and description',
|
|
2126
|
+
),
|
|
2127
|
+
]
|
|
2128
|
+
: []),
|
|
2129
|
+
...(frontmatter.name === name
|
|
2130
|
+
? []
|
|
2131
|
+
: [
|
|
2132
|
+
createPolicyViolation(
|
|
2133
|
+
'skill',
|
|
2134
|
+
skill,
|
|
2135
|
+
'SKILL.md frontmatter name matches its directory',
|
|
2136
|
+
),
|
|
2137
|
+
]),
|
|
2138
|
+
...(frontmatter.description === undefined || frontmatter.description.trim() === ''
|
|
2139
|
+
? [createPolicyViolation('skill', skill, 'SKILL.md description is non-empty')]
|
|
2140
|
+
: matchesSkillTrigger(frontmatter.description)
|
|
2141
|
+
? []
|
|
2142
|
+
: [
|
|
2143
|
+
createPolicyViolation(
|
|
2144
|
+
'skill',
|
|
2145
|
+
skill,
|
|
2146
|
+
'SKILL.md description names when to use the skill in a sentence beginning Use',
|
|
2147
|
+
),
|
|
2148
|
+
]),
|
|
2149
|
+
]),
|
|
2150
|
+
...inspectSkillTemplateTODOs('skill', skill, content),
|
|
2151
|
+
...inspectSkillImports(installation, skill, content),
|
|
2152
|
+
]
|
|
1562
2153
|
const hasMetadata = isPolicyFile(root, metadata)
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
'skill requires an exact-case regular agents/openai.yaml',
|
|
1569
|
-
),
|
|
1570
|
-
)
|
|
1571
|
-
} else {
|
|
1572
|
-
const prompt = parseSkillPrompt(readFileSync(join(root, metadata), 'utf8'))
|
|
1573
|
-
if (prompt === undefined) {
|
|
1574
|
-
violations.push(
|
|
1575
|
-
createPolicyViolation(
|
|
1576
|
-
'skill',
|
|
1577
|
-
metadata,
|
|
1578
|
-
'agents/openai.yaml matches the canonical four-line interface schema',
|
|
1579
|
-
),
|
|
1580
|
-
)
|
|
1581
|
-
} else if (!matchesSkillToken(prompt, name)) {
|
|
1582
|
-
violations.push(
|
|
2154
|
+
const prompt = hasMetadata
|
|
2155
|
+
? parseSkillPrompt(readFileSync(join(root, metadata), 'utf8'))
|
|
2156
|
+
: undefined
|
|
2157
|
+
const described = !hasMetadata
|
|
2158
|
+
? [
|
|
1583
2159
|
createPolicyViolation(
|
|
1584
2160
|
'skill',
|
|
1585
2161
|
metadata,
|
|
1586
|
-
|
|
2162
|
+
'skill requires an exact-case regular agents/openai.yaml',
|
|
1587
2163
|
),
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
const named = content === undefined ? [] : extractSkillReferences(content)
|
|
1592
|
-
if (content !== undefined) {
|
|
1593
|
-
for (const reference of named) {
|
|
1594
|
-
const path = `${base}/${reference}`
|
|
1595
|
-
if (!isPolicyFile(root, path)) {
|
|
1596
|
-
violations.push(
|
|
2164
|
+
]
|
|
2165
|
+
: prompt === undefined
|
|
2166
|
+
? [
|
|
1597
2167
|
createPolicyViolation(
|
|
1598
2168
|
'skill',
|
|
1599
|
-
|
|
1600
|
-
|
|
2169
|
+
metadata,
|
|
2170
|
+
'agents/openai.yaml matches the canonical four-line interface schema',
|
|
1601
2171
|
),
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
`${base}/${reference}`,
|
|
1618
|
-
`references Markdown file is named by SKILL.md: ${reference}`,
|
|
1619
|
-
),
|
|
1620
|
-
)
|
|
1621
|
-
}
|
|
1622
|
-
}
|
|
1623
|
-
const scripts = content === undefined ? [] : extractSkillScripts(content, name)
|
|
1624
|
-
for (const script of scripts) {
|
|
1625
|
-
if (!isPolicyFile(root, `${base}/${script}`)) {
|
|
1626
|
-
violations.push(
|
|
1627
|
-
createPolicyViolation(
|
|
1628
|
-
'skill',
|
|
1629
|
-
`${base}/${script}`,
|
|
1630
|
-
`SKILL.md script resolves to an exact-case regular file: ${script}`,
|
|
1631
|
-
),
|
|
1632
|
-
)
|
|
1633
|
-
}
|
|
1634
|
-
}
|
|
1635
|
-
for (const script of readSkillScripts(root, name)) {
|
|
1636
|
-
if (!scripts.includes(script)) {
|
|
1637
|
-
violations.push(
|
|
1638
|
-
createPolicyViolation(
|
|
1639
|
-
'skill',
|
|
1640
|
-
`${base}/${script}`,
|
|
1641
|
-
`scripts TypeScript file is named by SKILL.md: ${script}`,
|
|
1642
|
-
),
|
|
1643
|
-
)
|
|
1644
|
-
}
|
|
1645
|
-
const proof = `${SKILL_PROOF_ROOT}/${name}/${script.replace(/\.ts$/u, '.test.ts')}`
|
|
1646
|
-
if (!isPolicyFile(root, proof)) {
|
|
1647
|
-
violations.push(
|
|
1648
|
-
createPolicyViolation('skill', proof, `skill script has a mirrored proof: ${proof}`),
|
|
1649
|
-
)
|
|
1650
|
-
}
|
|
1651
|
-
}
|
|
1652
|
-
for (const script of content === undefined ? [] : extractForeignSkillScripts(content, name)) {
|
|
1653
|
-
if (!isPolicyFile(root, script)) {
|
|
1654
|
-
violations.push(
|
|
2172
|
+
]
|
|
2173
|
+
: matchesSkillToken(prompt, name)
|
|
2174
|
+
? []
|
|
2175
|
+
: [
|
|
2176
|
+
createPolicyViolation(
|
|
2177
|
+
'skill',
|
|
2178
|
+
metadata,
|
|
2179
|
+
`agents/openai.yaml default_prompt contains the complete token $${name}`,
|
|
2180
|
+
),
|
|
2181
|
+
]
|
|
2182
|
+
const named = content === undefined ? [] : extractSkillReferences(content)
|
|
2183
|
+
const referenced = named.flatMap((reference) => {
|
|
2184
|
+
const path = `${base}/${reference}`
|
|
2185
|
+
if (!isPolicyFile(root, path)) {
|
|
2186
|
+
return [
|
|
1655
2187
|
createPolicyViolation(
|
|
1656
2188
|
'skill',
|
|
1657
|
-
|
|
1658
|
-
`SKILL.md
|
|
2189
|
+
path,
|
|
2190
|
+
`SKILL.md reference resolves to an exact-case regular file: ${reference}`,
|
|
1659
2191
|
),
|
|
1660
|
-
|
|
1661
|
-
}
|
|
1662
|
-
}
|
|
1663
|
-
const references = resolvePolicyDirectory(root, `${base}/references`)
|
|
1664
|
-
if (references !== undefined) {
|
|
1665
|
-
for (const entry of readdirSync(references, { withFileTypes: true })) {
|
|
1666
|
-
if (entry.isDirectory()) {
|
|
1667
|
-
violations.push(
|
|
1668
|
-
createPolicyViolation(
|
|
1669
|
-
'skill',
|
|
1670
|
-
`${base}/references/${entry.name}`,
|
|
1671
|
-
'skill references directory contains no subdirectories',
|
|
1672
|
-
),
|
|
1673
|
-
)
|
|
1674
|
-
}
|
|
2192
|
+
]
|
|
1675
2193
|
}
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
(!hasSkill && path.toLowerCase() === 'skill.md') ||
|
|
1687
|
-
(!hasMetadata && path.toLowerCase() === 'agents/openai.yaml')
|
|
1688
|
-
) {
|
|
1689
|
-
continue
|
|
1690
|
-
}
|
|
1691
|
-
violations.push(
|
|
2194
|
+
const referenceContent = readFileSync(join(root, path), 'utf8')
|
|
2195
|
+
return [
|
|
2196
|
+
...inspectSkillTemplateTODOs('skill', path, referenceContent),
|
|
2197
|
+
...inspectSkillImports(installation, path, referenceContent),
|
|
2198
|
+
]
|
|
2199
|
+
})
|
|
2200
|
+
const unnamed = readSkillReferences(root, name).flatMap((reference) =>
|
|
2201
|
+
named.includes(reference)
|
|
2202
|
+
? []
|
|
2203
|
+
: [
|
|
1692
2204
|
createPolicyViolation(
|
|
1693
2205
|
'skill',
|
|
1694
|
-
`${base}/${
|
|
1695
|
-
|
|
2206
|
+
`${base}/${reference}`,
|
|
2207
|
+
`references Markdown file is named by SKILL.md: ${reference}`,
|
|
1696
2208
|
),
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
2209
|
+
],
|
|
2210
|
+
)
|
|
2211
|
+
const scripts = content === undefined ? [] : extractSkillScripts(content, name)
|
|
2212
|
+
const unresolved = scripts.flatMap((script) =>
|
|
2213
|
+
isPolicyFile(root, `${base}/${script}`)
|
|
2214
|
+
? []
|
|
2215
|
+
: [
|
|
1704
2216
|
createPolicyViolation(
|
|
1705
2217
|
'skill',
|
|
1706
|
-
`${base}/${
|
|
1707
|
-
|
|
2218
|
+
`${base}/${script}`,
|
|
2219
|
+
`SKILL.md script resolves to an exact-case regular file: ${script}`,
|
|
1708
2220
|
),
|
|
2221
|
+
],
|
|
2222
|
+
)
|
|
2223
|
+
const unproved = readSkillScripts(root, name).flatMap((script) => {
|
|
2224
|
+
const proof = `${SKILL_PROOF_ROOT}/${name}/${script.replace(/\.ts$/u, '.test.ts')}`
|
|
2225
|
+
return [
|
|
2226
|
+
...(scripts.includes(script)
|
|
2227
|
+
? []
|
|
2228
|
+
: [
|
|
2229
|
+
createPolicyViolation(
|
|
2230
|
+
'skill',
|
|
2231
|
+
`${base}/${script}`,
|
|
2232
|
+
`scripts TypeScript file is named by SKILL.md: ${script}`,
|
|
2233
|
+
),
|
|
2234
|
+
]),
|
|
2235
|
+
...(isPolicyFile(root, proof)
|
|
2236
|
+
? []
|
|
2237
|
+
: [createPolicyViolation('skill', proof, `skill script has a mirrored proof: ${proof}`)]),
|
|
2238
|
+
]
|
|
2239
|
+
})
|
|
2240
|
+
const foreign = (content === undefined ? [] : extractForeignSkillScripts(content, name)).flatMap(
|
|
2241
|
+
(script) =>
|
|
2242
|
+
isPolicyFile(root, script)
|
|
2243
|
+
? []
|
|
2244
|
+
: [
|
|
2245
|
+
createPolicyViolation(
|
|
2246
|
+
'skill',
|
|
2247
|
+
script,
|
|
2248
|
+
`SKILL.md names another skill's script that resolves to an exact-case regular file: ${script}`,
|
|
2249
|
+
),
|
|
2250
|
+
],
|
|
2251
|
+
)
|
|
2252
|
+
const references = resolvePolicyDirectory(root, `${base}/references`)
|
|
2253
|
+
const nested =
|
|
2254
|
+
references === undefined
|
|
2255
|
+
? []
|
|
2256
|
+
: readdirSync(references, { withFileTypes: true }).flatMap((entry) =>
|
|
2257
|
+
entry.isDirectory()
|
|
2258
|
+
? [
|
|
2259
|
+
createPolicyViolation(
|
|
2260
|
+
'skill',
|
|
2261
|
+
`${base}/references/${entry.name}`,
|
|
2262
|
+
'skill references directory contains no subdirectories',
|
|
2263
|
+
),
|
|
2264
|
+
]
|
|
2265
|
+
: [],
|
|
1709
2266
|
)
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
2267
|
+
const directory = resolvePolicyDirectory(root, base)
|
|
2268
|
+
const stray =
|
|
2269
|
+
directory === undefined
|
|
2270
|
+
? []
|
|
2271
|
+
: globSync('**/*', { cwd: directory })
|
|
2272
|
+
.map(normalizePolicyPath)
|
|
2273
|
+
.sort()
|
|
2274
|
+
.flatMap((path) => {
|
|
2275
|
+
if (resolvePolicyDirectory(directory, path) !== undefined) {
|
|
2276
|
+
return path === 'agents' ||
|
|
2277
|
+
path === 'references' ||
|
|
2278
|
+
path === 'scripts' ||
|
|
2279
|
+
path.startsWith('references/') ||
|
|
2280
|
+
(!hasSkill && path.toLowerCase() === 'skill.md') ||
|
|
2281
|
+
(!hasMetadata && path.toLowerCase() === 'agents/openai.yaml')
|
|
2282
|
+
? []
|
|
2283
|
+
: [
|
|
2284
|
+
createPolicyViolation(
|
|
2285
|
+
'skill',
|
|
2286
|
+
`${base}/${path}`,
|
|
2287
|
+
'skill directory contains only agents/, references/, and scripts/ directories',
|
|
2288
|
+
),
|
|
2289
|
+
]
|
|
2290
|
+
}
|
|
2291
|
+
if (!isPolicyFile(directory, path)) return []
|
|
2292
|
+
const file = basename(path).toLowerCase()
|
|
2293
|
+
if (file === 'readme.md' || file === 'changelog.md') {
|
|
2294
|
+
return [
|
|
2295
|
+
createPolicyViolation(
|
|
2296
|
+
'skill',
|
|
2297
|
+
`${base}/${path}`,
|
|
2298
|
+
'skill directory contains no README.md or CHANGELOG.md',
|
|
2299
|
+
),
|
|
2300
|
+
]
|
|
2301
|
+
}
|
|
2302
|
+
return path === 'SKILL.md' ||
|
|
2303
|
+
path === 'agents/openai.yaml' ||
|
|
2304
|
+
/^references\/[^/]+\.md$/u.test(path) ||
|
|
2305
|
+
/^scripts\/[^/]+\.ts$/u.test(path) ||
|
|
2306
|
+
(!hasSkill &&
|
|
2307
|
+
(path.toLowerCase() === 'skill.md' ||
|
|
2308
|
+
path.toLowerCase().startsWith('skill.md/'))) ||
|
|
2309
|
+
(!hasMetadata && path.toLowerCase() === 'agents/openai.yaml') ||
|
|
2310
|
+
(path.startsWith('references/') && path.slice('references/'.length).includes('/'))
|
|
2311
|
+
? []
|
|
2312
|
+
: [
|
|
2313
|
+
createPolicyViolation(
|
|
2314
|
+
'skill',
|
|
2315
|
+
`${base}/${path}`,
|
|
2316
|
+
'skill directory contains only SKILL.md, agents/openai.yaml, references/*.md, and scripts/*.ts',
|
|
2317
|
+
),
|
|
2318
|
+
]
|
|
2319
|
+
})
|
|
2320
|
+
return [
|
|
2321
|
+
...documented,
|
|
2322
|
+
...described,
|
|
2323
|
+
...referenced,
|
|
2324
|
+
...unnamed,
|
|
2325
|
+
...unresolved,
|
|
2326
|
+
...unproved,
|
|
2327
|
+
...foreign,
|
|
2328
|
+
...nested,
|
|
2329
|
+
...stray,
|
|
2330
|
+
]
|
|
1734
2331
|
}
|
|
1735
2332
|
|
|
1736
2333
|
/**
|
|
@@ -1741,10 +2338,7 @@ export function inspectSkill(
|
|
|
1741
2338
|
* @returns Every skill-family violation in directory and invariant order.
|
|
1742
2339
|
*/
|
|
1743
2340
|
export function inspectSkillFamily(root: string, installation = root): readonly PolicyViolation[] {
|
|
1744
|
-
|
|
1745
|
-
for (const name of readSkillFamily(root))
|
|
1746
|
-
violations.push(...inspectSkill(root, name, installation))
|
|
1747
|
-
return violations
|
|
2341
|
+
return readSkillFamily(root).flatMap((name) => inspectSkill(root, name, installation))
|
|
1748
2342
|
}
|
|
1749
2343
|
|
|
1750
2344
|
/**
|
|
@@ -1768,110 +2362,101 @@ export function inspectBridge(root: string, name: string): readonly PolicyViolat
|
|
|
1768
2362
|
const canonical = isPolicyFile(root, canonicalPath)
|
|
1769
2363
|
? parseSkillFrontmatter(readFileSync(join(root, canonicalPath), 'utf8'))
|
|
1770
2364
|
: undefined
|
|
1771
|
-
const
|
|
1772
|
-
if (bridge === undefined) {
|
|
1773
|
-
violations.push(
|
|
1774
|
-
createPolicyViolation('bridge', bridgePath, 'bridge SKILL.md frontmatter parses'),
|
|
1775
|
-
)
|
|
1776
|
-
} else {
|
|
1777
|
-
const keys = new Set(bridge.keys)
|
|
1778
|
-
if (
|
|
1779
|
-
bridge.keys.length !== 2 ||
|
|
1780
|
-
keys.size !== 2 ||
|
|
1781
|
-
!keys.has('name') ||
|
|
1782
|
-
!keys.has('description')
|
|
1783
|
-
) {
|
|
1784
|
-
violations.push(
|
|
1785
|
-
createPolicyViolation(
|
|
1786
|
-
'bridge',
|
|
1787
|
-
bridgePath,
|
|
1788
|
-
'bridge SKILL.md frontmatter contains exactly name and description',
|
|
1789
|
-
),
|
|
1790
|
-
)
|
|
1791
|
-
}
|
|
1792
|
-
}
|
|
1793
|
-
if (bridge !== undefined && canonical !== undefined) {
|
|
1794
|
-
if (bridge.source.name !== canonical.source.name) {
|
|
1795
|
-
violations.push(
|
|
1796
|
-
createPolicyViolation(
|
|
1797
|
-
'bridge',
|
|
1798
|
-
bridgePath,
|
|
1799
|
-
'bridge frontmatter name matches its canonical twin',
|
|
1800
|
-
),
|
|
1801
|
-
)
|
|
1802
|
-
}
|
|
1803
|
-
if (bridge.source.description !== canonical.source.description) {
|
|
1804
|
-
violations.push(
|
|
1805
|
-
createPolicyViolation(
|
|
1806
|
-
'bridge',
|
|
1807
|
-
bridgePath,
|
|
1808
|
-
'bridge frontmatter description matches its canonical twin',
|
|
1809
|
-
),
|
|
1810
|
-
)
|
|
1811
|
-
}
|
|
1812
|
-
}
|
|
2365
|
+
const keys = new Set(bridge?.keys)
|
|
1813
2366
|
const normalized = content.replaceAll('\r\n', '\n')
|
|
1814
2367
|
const boundary = normalized.indexOf('\n---', 3)
|
|
1815
2368
|
const body = boundary === -1 ? normalized : normalized.slice(boundary + '\n---'.length)
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
createPolicyViolation(
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
2369
|
+
return [
|
|
2370
|
+
...(bridge === undefined
|
|
2371
|
+
? [createPolicyViolation('bridge', bridgePath, 'bridge SKILL.md frontmatter parses')]
|
|
2372
|
+
: bridge.keys.length !== 2 || keys.size !== 2 || !keys.has('name') || !keys.has('description')
|
|
2373
|
+
? [
|
|
2374
|
+
createPolicyViolation(
|
|
2375
|
+
'bridge',
|
|
2376
|
+
bridgePath,
|
|
2377
|
+
'bridge SKILL.md frontmatter contains exactly name and description',
|
|
2378
|
+
),
|
|
2379
|
+
]
|
|
2380
|
+
: []),
|
|
2381
|
+
...(bridge !== undefined &&
|
|
2382
|
+
canonical !== undefined &&
|
|
2383
|
+
bridge.source.name !== canonical.source.name
|
|
2384
|
+
? [
|
|
2385
|
+
createPolicyViolation(
|
|
2386
|
+
'bridge',
|
|
2387
|
+
bridgePath,
|
|
2388
|
+
'bridge frontmatter name matches its canonical twin',
|
|
2389
|
+
),
|
|
2390
|
+
]
|
|
2391
|
+
: []),
|
|
2392
|
+
...(bridge !== undefined &&
|
|
2393
|
+
canonical !== undefined &&
|
|
2394
|
+
bridge.source.description !== canonical.source.description
|
|
2395
|
+
? [
|
|
2396
|
+
createPolicyViolation(
|
|
2397
|
+
'bridge',
|
|
2398
|
+
bridgePath,
|
|
2399
|
+
'bridge frontmatter description matches its canonical twin',
|
|
2400
|
+
),
|
|
2401
|
+
]
|
|
2402
|
+
: []),
|
|
2403
|
+
...(body.includes(canonicalPath)
|
|
2404
|
+
? []
|
|
2405
|
+
: [
|
|
2406
|
+
createPolicyViolation(
|
|
2407
|
+
'bridge',
|
|
2408
|
+
bridgePath,
|
|
2409
|
+
`bridge body names its canonical workflow: ${canonicalPath}`,
|
|
2410
|
+
),
|
|
2411
|
+
]),
|
|
2412
|
+
...inspectSkillTemplateTODOs('bridge', bridgePath, content),
|
|
2413
|
+
...(resolvePolicyDirectory(root, `${bridgeBase}/references`) === undefined
|
|
2414
|
+
? []
|
|
2415
|
+
: [
|
|
2416
|
+
createPolicyViolation(
|
|
2417
|
+
'bridge',
|
|
2418
|
+
`${bridgeBase}/references`,
|
|
2419
|
+
'bridge owns no references directory',
|
|
2420
|
+
),
|
|
2421
|
+
]),
|
|
2422
|
+
]
|
|
1836
2423
|
}
|
|
1837
2424
|
|
|
1838
2425
|
/**
|
|
1839
|
-
* Inspects the provider bridge set and every bridge shared with the canonical skill family.
|
|
1840
|
-
*
|
|
1841
|
-
* @param root - The workspace root to inspect.
|
|
1842
|
-
* @returns Every bridge-set and bridge-content violation in directory order.
|
|
1843
|
-
*/
|
|
1844
|
-
export function inspectSkillBridges(root: string): readonly PolicyViolation[] {
|
|
1845
|
-
const canonical = readSkillFamily(root)
|
|
1846
|
-
const bridges = readPolicyDirectories(root, SKILL_BRIDGE_ROOT)
|
|
1847
|
-
const bridgeSet = new Set(bridges)
|
|
1848
|
-
const canonicalSet = new Set(canonical)
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
}
|
|
1874
|
-
return violations
|
|
2426
|
+
* Inspects the provider bridge set and every bridge shared with the canonical skill family.
|
|
2427
|
+
*
|
|
2428
|
+
* @param root - The workspace root to inspect.
|
|
2429
|
+
* @returns Every bridge-set and bridge-content violation in directory order.
|
|
2430
|
+
*/
|
|
2431
|
+
export function inspectSkillBridges(root: string): readonly PolicyViolation[] {
|
|
2432
|
+
const canonical = readSkillFamily(root)
|
|
2433
|
+
const bridges = readPolicyDirectories(root, SKILL_BRIDGE_ROOT)
|
|
2434
|
+
const bridgeSet = new Set(bridges)
|
|
2435
|
+
const canonicalSet = new Set(canonical)
|
|
2436
|
+
return [
|
|
2437
|
+
...canonical.flatMap((name) =>
|
|
2438
|
+
bridgeSet.has(name)
|
|
2439
|
+
? inspectBridge(root, name)
|
|
2440
|
+
: [
|
|
2441
|
+
createPolicyViolation(
|
|
2442
|
+
'bridge',
|
|
2443
|
+
`${SKILL_BRIDGE_ROOT}/${name}`,
|
|
2444
|
+
'canonical skill has a matching provider bridge directory',
|
|
2445
|
+
),
|
|
2446
|
+
],
|
|
2447
|
+
),
|
|
2448
|
+
...bridges.flatMap((name) =>
|
|
2449
|
+
canonicalSet.has(name)
|
|
2450
|
+
? []
|
|
2451
|
+
: [
|
|
2452
|
+
createPolicyViolation(
|
|
2453
|
+
'bridge',
|
|
2454
|
+
`${SKILL_BRIDGE_ROOT}/${name}`,
|
|
2455
|
+
'provider bridge directory has a canonical skill twin',
|
|
2456
|
+
),
|
|
2457
|
+
],
|
|
2458
|
+
),
|
|
2459
|
+
]
|
|
1875
2460
|
}
|
|
1876
2461
|
|
|
1877
2462
|
/**
|
|
@@ -1884,14 +2469,10 @@ export function readPolicyRuleMap(content: string): readonly string[] {
|
|
|
1884
2469
|
const lines = content.replaceAll('\r\n', '\n').split('\n')
|
|
1885
2470
|
const heading = lines.indexOf(POLICY_RULE_MAP_HEADING)
|
|
1886
2471
|
if (heading === -1) return []
|
|
1887
|
-
|
|
1888
|
-
for (let index = heading + 1; index < lines.length; index += 1) {
|
|
1889
|
-
const line = lines[index]
|
|
1890
|
-
if (line === undefined || line.startsWith('## ')) break
|
|
2472
|
+
return extractPolicySection(lines, heading).flatMap((line) => {
|
|
1891
2473
|
const cell = line.match(/^\|\s*`([^`]+)`\s*\|/u)?.[1]
|
|
1892
|
-
|
|
1893
|
-
}
|
|
1894
|
-
return paths
|
|
2474
|
+
return cell === undefined ? [] : [normalizePolicyPath(cell)]
|
|
2475
|
+
})
|
|
1895
2476
|
}
|
|
1896
2477
|
|
|
1897
2478
|
/**
|
|
@@ -1914,20 +2495,18 @@ export function inspectPolicyRuleMap(root: string): readonly PolicyViolation[] {
|
|
|
1914
2495
|
? readFileSync(join(root, POLICY_RULE_MAP_FILE), 'utf8')
|
|
1915
2496
|
: ''
|
|
1916
2497
|
const registered = new Set(readPolicyRuleMap(content))
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
createPolicyViolation('rules', path, 'every rule-map row resolves to a rule file'),
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
}
|
|
1930
|
-
return violations
|
|
2498
|
+
return [
|
|
2499
|
+
...rules.flatMap((rule) =>
|
|
2500
|
+
registered.has(rule)
|
|
2501
|
+
? []
|
|
2502
|
+
: [createPolicyViolation('rules', rule, 'the rule map names every rule file')],
|
|
2503
|
+
),
|
|
2504
|
+
...[...registered].flatMap((path) =>
|
|
2505
|
+
isPolicyFile(root, path)
|
|
2506
|
+
? []
|
|
2507
|
+
: [createPolicyViolation('rules', path, 'every rule-map row resolves to a rule file')],
|
|
2508
|
+
),
|
|
2509
|
+
]
|
|
1931
2510
|
}
|
|
1932
2511
|
|
|
1933
2512
|
/**
|
|
@@ -1941,50 +2520,53 @@ export function inspectPolicyRuleMap(root: string): readonly PolicyViolation[] {
|
|
|
1941
2520
|
* @returns Every unusable-name and case-collision violation in path order.
|
|
1942
2521
|
*/
|
|
1943
2522
|
export function inspectPolicyFilenamePaths(paths: readonly string[]): readonly PolicyViolation[] {
|
|
1944
|
-
|
|
1945
|
-
const
|
|
1946
|
-
|
|
1947
|
-
|
|
2523
|
+
// The iterator visits a hole as `undefined`, which normalization refuses, as the loop did.
|
|
2524
|
+
const normalized = Iterator.from(paths).map(normalizePolicyPath).toArray()
|
|
2525
|
+
// Built from the reversed list, so the first path that folds to a key is the spelling a collision reports.
|
|
2526
|
+
const folded = new Map(normalized.toReversed().map((path) => [path.toLowerCase(), path]))
|
|
2527
|
+
return normalized.flatMap((path) => {
|
|
1948
2528
|
const segment = basename(path)
|
|
1949
2529
|
const [stem = ''] = segment.split('.')
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
2530
|
+
const previous = folded.get(path.toLowerCase())
|
|
2531
|
+
return [
|
|
2532
|
+
...(POLICY_RESERVED_NAMES.includes(stem.toLowerCase())
|
|
2533
|
+
? [
|
|
2534
|
+
createPolicyViolation(
|
|
2535
|
+
'portability',
|
|
2536
|
+
path,
|
|
2537
|
+
'path segments avoid the names Windows reserves',
|
|
2538
|
+
),
|
|
2539
|
+
]
|
|
2540
|
+
: []),
|
|
2541
|
+
...(POLICY_RESERVED_PATTERN.test(segment)
|
|
2542
|
+
? [
|
|
2543
|
+
createPolicyViolation(
|
|
2544
|
+
'portability',
|
|
2545
|
+
path,
|
|
2546
|
+
'path segments avoid the characters Windows refuses',
|
|
2547
|
+
),
|
|
2548
|
+
]
|
|
2549
|
+
: []),
|
|
2550
|
+
...(segment.endsWith('.') || segment.endsWith(' ')
|
|
2551
|
+
? [
|
|
2552
|
+
createPolicyViolation(
|
|
2553
|
+
'portability',
|
|
2554
|
+
path,
|
|
2555
|
+
'path segments end with neither a dot nor a space',
|
|
2556
|
+
),
|
|
2557
|
+
]
|
|
2558
|
+
: []),
|
|
2559
|
+
...(previous === undefined || previous === path
|
|
2560
|
+
? []
|
|
2561
|
+
: [
|
|
2562
|
+
createPolicyViolation(
|
|
2563
|
+
'portability',
|
|
2564
|
+
path,
|
|
2565
|
+
`path differs from ${previous} by case alone`,
|
|
2566
|
+
),
|
|
2567
|
+
]),
|
|
2568
|
+
]
|
|
2569
|
+
})
|
|
1988
2570
|
}
|
|
1989
2571
|
|
|
1990
2572
|
/**
|
|
@@ -2014,21 +2596,16 @@ export function inspectPolicyFilenames(root: string): readonly PolicyViolation[]
|
|
|
2014
2596
|
* @returns Each script name and its command, in manifest order.
|
|
2015
2597
|
*/
|
|
2016
2598
|
export function parsePolicyScripts(content: string): ReadonlyMap<string, string> {
|
|
2017
|
-
const
|
|
2018
|
-
|
|
2019
|
-
try {
|
|
2020
|
-
manifest = JSON.parse(content)
|
|
2021
|
-
} catch {
|
|
2022
|
-
return scripts
|
|
2023
|
-
}
|
|
2024
|
-
if (!isPolicyRecord(manifest)) return scripts
|
|
2599
|
+
const manifest = parsePolicyJSON(content)
|
|
2600
|
+
if (!isPolicyRecord(manifest)) return new Map()
|
|
2025
2601
|
const record: unknown = Object.getOwnPropertyDescriptor(manifest, 'scripts')?.value
|
|
2026
|
-
if (!isPolicyRecord(record)) return
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2602
|
+
if (!isPolicyRecord(record)) return new Map()
|
|
2603
|
+
return new Map(
|
|
2604
|
+
Object.getOwnPropertyNames(record).flatMap((name): Array<[string, string]> => {
|
|
2605
|
+
const command: unknown = Object.getOwnPropertyDescriptor(record, name)?.value
|
|
2606
|
+
return typeof command === 'string' ? [[name, command]] : []
|
|
2607
|
+
}),
|
|
2608
|
+
)
|
|
2032
2609
|
}
|
|
2033
2610
|
|
|
2034
2611
|
/**
|
|
@@ -2040,19 +2617,17 @@ export function parsePolicyScripts(content: string): ReadonlyMap<string, string>
|
|
|
2040
2617
|
export function inspectPolicyScripts(root: string): readonly PolicyViolation[] {
|
|
2041
2618
|
if (!isPolicyFile(root, POLICY_MANIFEST_FILE)) return []
|
|
2042
2619
|
const content = readFileSync(join(root, POLICY_MANIFEST_FILE), 'utf8')
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
}
|
|
2055
|
-
return violations
|
|
2620
|
+
return [...parsePolicyScripts(content)].flatMap(([name, command]) =>
|
|
2621
|
+
POLICY_SHELL_PATTERN.test(command)
|
|
2622
|
+
? [
|
|
2623
|
+
createPolicyViolation(
|
|
2624
|
+
'portability',
|
|
2625
|
+
POLICY_MANIFEST_FILE,
|
|
2626
|
+
`manifest scripts name no .sh file: ${name}`,
|
|
2627
|
+
),
|
|
2628
|
+
]
|
|
2629
|
+
: [],
|
|
2630
|
+
)
|
|
2056
2631
|
}
|
|
2057
2632
|
|
|
2058
2633
|
/**
|
|
@@ -2091,12 +2666,7 @@ export function readPolicyProse(root: string): readonly string[] {
|
|
|
2091
2666
|
*/
|
|
2092
2667
|
export function readPolicyPackage(root: string): string | undefined {
|
|
2093
2668
|
if (!isPolicyFile(root, POLICY_MANIFEST_FILE)) return undefined
|
|
2094
|
-
|
|
2095
|
-
try {
|
|
2096
|
-
manifest = JSON.parse(readFileSync(join(root, POLICY_MANIFEST_FILE), 'utf8'))
|
|
2097
|
-
} catch {
|
|
2098
|
-
return undefined
|
|
2099
|
-
}
|
|
2669
|
+
const manifest = readPolicyJSON(join(root, POLICY_MANIFEST_FILE))
|
|
2100
2670
|
if (!isPolicyRecord(manifest)) return undefined
|
|
2101
2671
|
const name: unknown = Object.getOwnPropertyDescriptor(manifest, 'name')?.value
|
|
2102
2672
|
if (typeof name !== 'string') return undefined
|
|
@@ -2123,14 +2693,10 @@ export function readPolicyCatalog(
|
|
|
2123
2693
|
const lines = readFileSync(join(root, path), 'utf8').replaceAll('\r\n', '\n').split('\n')
|
|
2124
2694
|
const heading = lines.indexOf(POLICY_CATALOG_HEADING)
|
|
2125
2695
|
if (heading === -1) return []
|
|
2126
|
-
|
|
2127
|
-
for (let index = heading + 1; index < lines.length; index += 1) {
|
|
2128
|
-
const line = lines[index]
|
|
2129
|
-
if (line === undefined || line.startsWith('## ')) break
|
|
2696
|
+
return extractPolicySection(lines, heading).flatMap((line) => {
|
|
2130
2697
|
const cell = line.match(/^\|\s*`([^`]+)`\s*\|/u)?.[1]
|
|
2131
|
-
|
|
2132
|
-
}
|
|
2133
|
-
return names
|
|
2698
|
+
return cell === undefined ? [] : [cell.slice(cell.lastIndexOf('/') + 1)]
|
|
2699
|
+
})
|
|
2134
2700
|
}
|
|
2135
2701
|
|
|
2136
2702
|
/**
|
|
@@ -2200,31 +2766,30 @@ export function isPolicyStray(root: string, path: string): boolean {
|
|
|
2200
2766
|
* order.
|
|
2201
2767
|
*/
|
|
2202
2768
|
export function inspectPolicyProse(root: string): readonly PolicyViolation[] {
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
if (isPolicyMirror(root, path)) continue
|
|
2769
|
+
return readPolicyProse(root).flatMap((path) => {
|
|
2770
|
+
const stray = isPolicyStray(root, path)
|
|
2771
|
+
? [
|
|
2772
|
+
createPolicyViolation(
|
|
2773
|
+
'prose',
|
|
2774
|
+
path,
|
|
2775
|
+
"guide is the package's own, the map, or a catalog row",
|
|
2776
|
+
),
|
|
2777
|
+
]
|
|
2778
|
+
: []
|
|
2779
|
+
if (isPolicyMirror(root, path)) return stray
|
|
2215
2780
|
const prose = stripPolicyCode(readFileSync(join(root, path), 'utf8'))
|
|
2216
|
-
|
|
2217
|
-
|
|
2781
|
+
return [
|
|
2782
|
+
...stray,
|
|
2783
|
+
...textToPolicyHits(prose).map((hit) =>
|
|
2218
2784
|
createPolicyViolation(
|
|
2219
2785
|
'prose',
|
|
2220
2786
|
path,
|
|
2221
2787
|
`prose carries no banned term: ${hit.term.term} (${hit.term.replacement})`,
|
|
2222
2788
|
prose.slice(0, hit.index).split('\n').length,
|
|
2223
2789
|
),
|
|
2224
|
-
)
|
|
2225
|
-
|
|
2226
|
-
}
|
|
2227
|
-
return violations
|
|
2790
|
+
),
|
|
2791
|
+
]
|
|
2792
|
+
})
|
|
2228
2793
|
}
|
|
2229
2794
|
|
|
2230
2795
|
/**
|
|
@@ -2241,18 +2806,12 @@ export function readPolicyTerms(content: string): readonly string[] {
|
|
|
2241
2806
|
const lines = content.replaceAll('\r\n', '\n').split('\n')
|
|
2242
2807
|
const heading = lines.indexOf(POLICY_TERM_HEADING)
|
|
2243
2808
|
if (heading === -1) return []
|
|
2244
|
-
|
|
2245
|
-
for (let index = heading + 1; index < lines.length; index += 1) {
|
|
2246
|
-
const line = lines[index]
|
|
2247
|
-
if (line === undefined || line.startsWith('## ')) break
|
|
2809
|
+
return extractPolicySection(lines, heading).flatMap((line) => {
|
|
2248
2810
|
const cell = line.match(/^\|([^|]*)\|/u)?.[1]
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
}
|
|
2254
|
-
}
|
|
2255
|
-
return terms
|
|
2811
|
+
return cell === undefined
|
|
2812
|
+
? []
|
|
2813
|
+
: [...cell.matchAll(/`([^`]+)`/gu)].flatMap((match) => match[1] ?? [])
|
|
2814
|
+
})
|
|
2256
2815
|
}
|
|
2257
2816
|
|
|
2258
2817
|
/**
|
|
@@ -2272,7 +2831,7 @@ export function inspectPolicyPortability(root: string): readonly PolicyViolation
|
|
|
2272
2831
|
/**
|
|
2273
2832
|
* Locates parsed exports at their physical declaration lines.
|
|
2274
2833
|
*
|
|
2275
|
-
* @param
|
|
2834
|
+
* @param written - The declaring workspace-relative path, normalized before it is read.
|
|
2276
2835
|
* @param text - The declaration source text.
|
|
2277
2836
|
* @param root - The workspace root used to resolve relative star exports, when supplied.
|
|
2278
2837
|
* @param ancestors - The source paths already visited along this star-export branch.
|
|
@@ -2280,17 +2839,16 @@ export function inspectPolicyPortability(root: string): readonly PolicyViolation
|
|
|
2280
2839
|
* @throws An `Error` when syntax, an export form, or a star target cannot be read.
|
|
2281
2840
|
*/
|
|
2282
2841
|
export function readPolicyDeclarations(
|
|
2283
|
-
|
|
2842
|
+
written: string,
|
|
2284
2843
|
text: string,
|
|
2285
2844
|
root?: string,
|
|
2286
2845
|
ancestors: readonly string[] = [],
|
|
2287
2846
|
): readonly PolicySurfaceDeclaration[] {
|
|
2288
|
-
path = normalizePolicyPath(
|
|
2847
|
+
const path = normalizePolicyPath(written)
|
|
2289
2848
|
if (ancestors.includes(path)) return []
|
|
2290
2849
|
const source = parseSync(path, text)
|
|
2291
2850
|
if (source.errors.length > 0) throw new Error(`export syntax is unreadable at ${path}`)
|
|
2292
|
-
|
|
2293
|
-
for (const statement of source.program.body) {
|
|
2851
|
+
return source.program.body.flatMap((statement): readonly PolicySurfaceDeclaration[] => {
|
|
2294
2852
|
const line = text.slice(0, statement.start).split(/\r\n|\n/u).length
|
|
2295
2853
|
if (statement.type === 'ExportAllDeclaration') {
|
|
2296
2854
|
if (statement.exported !== null) {
|
|
@@ -2299,8 +2857,7 @@ export function readPolicyDeclarations(
|
|
|
2299
2857
|
? statement.exported.name
|
|
2300
2858
|
: statement.exported.value
|
|
2301
2859
|
if (name === 'default') throw new Error(`default export is unsupported at ${path}:${line}`)
|
|
2302
|
-
|
|
2303
|
-
continue
|
|
2860
|
+
return [{ name, path, line }]
|
|
2304
2861
|
}
|
|
2305
2862
|
const target = statement.source.value
|
|
2306
2863
|
const resolved = resolveLink(
|
|
@@ -2315,40 +2872,35 @@ export function readPolicyDeclarations(
|
|
|
2315
2872
|
) {
|
|
2316
2873
|
throw new Error(`star export target is unreadable at ${path}:${line}: ${target}`)
|
|
2317
2874
|
}
|
|
2318
|
-
|
|
2319
|
-
...
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
]),
|
|
2323
|
-
)
|
|
2324
|
-
continue
|
|
2875
|
+
return readPolicyDeclarations(resolved, readFileSync(join(root, resolved), 'utf8'), root, [
|
|
2876
|
+
...ancestors,
|
|
2877
|
+
path,
|
|
2878
|
+
])
|
|
2325
2879
|
}
|
|
2326
2880
|
if (statement.type === 'ExportNamedDeclaration') {
|
|
2327
|
-
|
|
2881
|
+
const listed = statement.specifiers.map((specifier) => {
|
|
2328
2882
|
const name =
|
|
2329
2883
|
specifier.exported.type === 'Identifier'
|
|
2330
2884
|
? specifier.exported.name
|
|
2331
2885
|
: specifier.exported.value
|
|
2332
2886
|
if (name === 'default') throw new Error(`default export is unsupported at ${path}:${line}`)
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
path,
|
|
2336
|
-
line: text.slice(0, specifier.start).split(/\r\n|\n/u).length,
|
|
2337
|
-
})
|
|
2338
|
-
}
|
|
2887
|
+
return { name, path, line: text.slice(0, specifier.start).split(/\r\n|\n/u).length }
|
|
2888
|
+
})
|
|
2339
2889
|
const declaration = statement.declaration
|
|
2340
|
-
if (declaration === null)
|
|
2890
|
+
if (declaration === null) return listed
|
|
2341
2891
|
if (declaration.type === 'VariableDeclaration') {
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2892
|
+
return [
|
|
2893
|
+
...listed,
|
|
2894
|
+
...declaration.declarations.map((variable) => {
|
|
2895
|
+
if (variable.id.type !== 'Identifier')
|
|
2896
|
+
throw new Error(`export binding is unsupported at ${path}:${line}`)
|
|
2897
|
+
return {
|
|
2898
|
+
name: variable.id.name,
|
|
2899
|
+
path,
|
|
2900
|
+
line: text.slice(0, variable.id.start).split(/\r\n|\n/u).length,
|
|
2901
|
+
}
|
|
2902
|
+
}),
|
|
2903
|
+
]
|
|
2352
2904
|
}
|
|
2353
2905
|
if (
|
|
2354
2906
|
declaration.type === 'FunctionDeclaration' ||
|
|
@@ -2361,8 +2913,7 @@ export function readPolicyDeclarations(
|
|
|
2361
2913
|
) {
|
|
2362
2914
|
if (declaration.id?.type !== 'Identifier')
|
|
2363
2915
|
throw new Error(`export name is unreadable at ${path}:${line}`)
|
|
2364
|
-
|
|
2365
|
-
continue
|
|
2916
|
+
return [...listed, { name: declaration.id.name, path, line }]
|
|
2366
2917
|
}
|
|
2367
2918
|
throw new Error(`export declaration is unsupported at ${path}:${line}: ${declaration.type}`)
|
|
2368
2919
|
}
|
|
@@ -2373,8 +2924,8 @@ export function readPolicyDeclarations(
|
|
|
2373
2924
|
) {
|
|
2374
2925
|
throw new Error(`export statement is unsupported at ${path}:${line}: ${statement.type}`)
|
|
2375
2926
|
}
|
|
2376
|
-
|
|
2377
|
-
|
|
2927
|
+
return []
|
|
2928
|
+
})
|
|
2378
2929
|
}
|
|
2379
2930
|
|
|
2380
2931
|
/**
|
|
@@ -2414,21 +2965,21 @@ export function collectPolicyDeclarations(
|
|
|
2414
2965
|
* exactly one bare `./index.scss` import, including styles extensions and themes.
|
|
2415
2966
|
*/
|
|
2416
2967
|
export function readPolicySurface(root: string): PolicySurfacePopulation {
|
|
2417
|
-
const files
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2968
|
+
const files = Object.fromEntries(
|
|
2969
|
+
globSync('src/**/*.ts', { cwd: root })
|
|
2970
|
+
.map(normalizePolicyPath)
|
|
2971
|
+
.sort()
|
|
2972
|
+
.map((path): [string, string] => [path, readFileSync(join(root, path), 'utf8')]),
|
|
2973
|
+
)
|
|
2421
2974
|
const barrels = Object.keys(files).filter((path) => path.endsWith('/index.ts'))
|
|
2422
|
-
const targets = new Set<string>()
|
|
2423
|
-
const violations: PolicyViolation[] = []
|
|
2424
2975
|
const sheets = new Set([
|
|
2425
2976
|
POLICY_SURFACE_STYLES_ENTRY,
|
|
2426
2977
|
...collectSheets(root).map((face) => `src/${face}/sheet.ts`),
|
|
2427
2978
|
'src/styles/themes/sheet.ts',
|
|
2428
2979
|
])
|
|
2429
|
-
|
|
2980
|
+
const entryViolations = [...sheets].flatMap((path) => {
|
|
2430
2981
|
const stylesEntry = files[path]
|
|
2431
|
-
if (stylesEntry === undefined)
|
|
2982
|
+
if (stylesEntry === undefined) return []
|
|
2432
2983
|
const stylesSource = parseSync(path, stylesEntry)
|
|
2433
2984
|
const stylesStatement =
|
|
2434
2985
|
stylesSource.errors.length === 0 ? stylesSource.program.body[0] : undefined
|
|
@@ -2438,79 +2989,85 @@ export function readPolicySurface(root: string): PolicySurfacePopulation {
|
|
|
2438
2989
|
stylesStatement?.type === 'ImportDeclaration' &&
|
|
2439
2990
|
stylesStatement.specifiers.length === 0 &&
|
|
2440
2991
|
stylesStatement.source.value === './index.scss'
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
}
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
const end = text.slice(0, statement.end).split(/\r\n|\n/u).length - 1
|
|
2467
|
-
const row = lines[start]?.code.match(POLICY_SURFACE_BARREL_PATTERN)
|
|
2468
|
-
const target = row?.[1] ?? row?.[2]
|
|
2469
|
-
if (
|
|
2470
|
-
statement.type !== 'ExportAllDeclaration' ||
|
|
2471
|
-
statement.exportKind !== 'value' ||
|
|
2472
|
-
statement.exported !== null ||
|
|
2473
|
-
start !== end ||
|
|
2474
|
-
target === undefined
|
|
2475
|
-
) {
|
|
2476
|
-
violations.push(
|
|
2477
|
-
createPolicyViolation(
|
|
2478
|
-
'surface',
|
|
2479
|
-
path,
|
|
2480
|
-
'surface population incomplete: barrel requires a relative .js star export on one line',
|
|
2481
|
-
start + 1,
|
|
2482
|
-
),
|
|
2483
|
-
)
|
|
2484
|
-
continue
|
|
2485
|
-
}
|
|
2486
|
-
const resolved = resolveLink(path, `${target.slice(0, -3)}.ts`)
|
|
2487
|
-
if (
|
|
2488
|
-
!hasCanonicalSegments(resolved) ||
|
|
2489
|
-
!resolved.startsWith('src/') ||
|
|
2490
|
-
files[resolved] === undefined
|
|
2491
|
-
) {
|
|
2492
|
-
violations.push(
|
|
2493
|
-
createPolicyViolation(
|
|
2494
|
-
'surface',
|
|
2495
|
-
path,
|
|
2496
|
-
`surface population incomplete: barrel target is unreadable: ${target}`,
|
|
2497
|
-
start + 1,
|
|
2498
|
-
),
|
|
2499
|
-
)
|
|
2500
|
-
continue
|
|
2992
|
+
const line =
|
|
2993
|
+
stylesStatement === undefined
|
|
2994
|
+
? 1
|
|
2995
|
+
: stylesEntry.slice(0, stylesStatement.start).split(/\r\n|\n/u).length
|
|
2996
|
+
return stylesValid
|
|
2997
|
+
? []
|
|
2998
|
+
: [createPolicyViolation('surface', path, POLICY_SURFACE_STYLES_MESSAGE, line)]
|
|
2999
|
+
})
|
|
3000
|
+
// Each barrel statement answers one violation, one star-export target, or neither.
|
|
3001
|
+
const readings = barrels.flatMap(
|
|
3002
|
+
(path): ReadonlyArray<{ readonly violation?: PolicyViolation; readonly target?: string }> => {
|
|
3003
|
+
const text = files[path]
|
|
3004
|
+
if (text === undefined) return []
|
|
3005
|
+
const source = parseSync(path, text)
|
|
3006
|
+
const lines = extractSourceLines(text)
|
|
3007
|
+
if (source.errors.length > 0) {
|
|
3008
|
+
return [
|
|
3009
|
+
{
|
|
3010
|
+
violation: createPolicyViolation(
|
|
3011
|
+
'surface',
|
|
3012
|
+
path,
|
|
3013
|
+
'surface population incomplete: barrel syntax is unreadable',
|
|
3014
|
+
),
|
|
3015
|
+
},
|
|
3016
|
+
]
|
|
2501
3017
|
}
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
3018
|
+
return source.program.body.map((statement) => {
|
|
3019
|
+
const start = text.slice(0, statement.start).split(/\r\n|\n/u).length - 1
|
|
3020
|
+
const end = text.slice(0, statement.end).split(/\r\n|\n/u).length - 1
|
|
3021
|
+
const row = lines[start]?.code.match(POLICY_SURFACE_BARREL_PATTERN)
|
|
3022
|
+
const target = row?.[1] ?? row?.[2]
|
|
3023
|
+
if (
|
|
3024
|
+
statement.type !== 'ExportAllDeclaration' ||
|
|
3025
|
+
statement.exportKind !== 'value' ||
|
|
3026
|
+
statement.exported !== null ||
|
|
3027
|
+
start !== end ||
|
|
3028
|
+
target === undefined
|
|
3029
|
+
) {
|
|
3030
|
+
return {
|
|
3031
|
+
violation: createPolicyViolation(
|
|
3032
|
+
'surface',
|
|
3033
|
+
path,
|
|
3034
|
+
'surface population incomplete: barrel requires a relative .js star export on one line',
|
|
3035
|
+
start + 1,
|
|
3036
|
+
),
|
|
3037
|
+
}
|
|
3038
|
+
}
|
|
3039
|
+
const resolved = resolveLink(path, `${target.slice(0, -3)}.ts`)
|
|
3040
|
+
if (
|
|
3041
|
+
!hasCanonicalSegments(resolved) ||
|
|
3042
|
+
!resolved.startsWith('src/') ||
|
|
3043
|
+
files[resolved] === undefined
|
|
3044
|
+
) {
|
|
3045
|
+
return {
|
|
3046
|
+
violation: createPolicyViolation(
|
|
3047
|
+
'surface',
|
|
3048
|
+
path,
|
|
3049
|
+
`surface population incomplete: barrel target is unreadable: ${target}`,
|
|
3050
|
+
start + 1,
|
|
3051
|
+
),
|
|
3052
|
+
}
|
|
3053
|
+
}
|
|
3054
|
+
return resolved.endsWith('/index.ts') ? {} : { target: resolved }
|
|
3055
|
+
})
|
|
3056
|
+
},
|
|
3057
|
+
)
|
|
3058
|
+
const targets = new Set(readings.flatMap(({ target }) => (target === undefined ? [] : [target])))
|
|
3059
|
+
const reads = [...targets].flatMap((path) => {
|
|
2507
3060
|
const text = files[path]
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
3061
|
+
return text === undefined ? [] : [collectPolicyDeclarations(root, path, text)]
|
|
3062
|
+
})
|
|
3063
|
+
return {
|
|
3064
|
+
declarations: reads.flatMap((read) => read.declarations),
|
|
3065
|
+
violations: [
|
|
3066
|
+
...entryViolations,
|
|
3067
|
+
...readings.flatMap(({ violation }) => (violation === undefined ? [] : [violation])),
|
|
3068
|
+
...reads.flatMap((read) => (read.violation === undefined ? [] : [read.violation])),
|
|
3069
|
+
],
|
|
2512
3070
|
}
|
|
2513
|
-
return { declarations, violations }
|
|
2514
3071
|
}
|
|
2515
3072
|
|
|
2516
3073
|
/**
|
|
@@ -2541,79 +3098,97 @@ export function inspectPolicySurface(root: string): readonly PolicyViolation[] {
|
|
|
2541
3098
|
),
|
|
2542
3099
|
]
|
|
2543
3100
|
}
|
|
2544
|
-
const
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
}
|
|
2555
|
-
}
|
|
2556
|
-
const grandfather = new Set<string>()
|
|
2557
|
-
const owners = new Map<string, Set<string>>()
|
|
2558
|
-
for (const path of globSync('guides/*.md', { cwd: host }).map(normalizePolicyPath).sort()) {
|
|
2559
|
-
const owner = basename(path, '.md')
|
|
2560
|
-
if (owner === POLICY_GUIDE_MAP) continue
|
|
2561
|
-
const guide = createGuide(readFileSync(join(host, path), 'utf8'))
|
|
2562
|
-
if (!guide.sections().includes('Surface')) {
|
|
2563
|
-
violations.push(
|
|
2564
|
-
createPolicyViolation(
|
|
2565
|
-
'surface',
|
|
2566
|
-
`${directory}/${owner}.md`,
|
|
2567
|
-
'surface evidence missing: hosted guide has no Surface section',
|
|
2568
|
-
),
|
|
2569
|
-
)
|
|
2570
|
-
continue
|
|
2571
|
-
}
|
|
2572
|
-
for (const symbol of guide.surface()) {
|
|
2573
|
-
if (owner === own) {
|
|
2574
|
-
grandfather.add(symbol.name)
|
|
2575
|
-
continue
|
|
2576
|
-
}
|
|
2577
|
-
const claimed = owners.get(symbol.name) ?? new Set<string>()
|
|
2578
|
-
claimed.add(owner)
|
|
2579
|
-
owners.set(symbol.name, claimed)
|
|
2580
|
-
}
|
|
2581
|
-
}
|
|
2582
|
-
const population = readPolicySurface(root)
|
|
2583
|
-
violations.push(...population.violations)
|
|
2584
|
-
const declarations = population.declarations.filter(
|
|
2585
|
-
(declaration) => !grandfather.has(declaration.name),
|
|
3101
|
+
const missing = [...names].flatMap((name) =>
|
|
3102
|
+
isPolicyFile(host, `guides/${name}.md`)
|
|
3103
|
+
? []
|
|
3104
|
+
: [
|
|
3105
|
+
createPolicyViolation(
|
|
3106
|
+
'surface',
|
|
3107
|
+
`${directory}/${name}.md`,
|
|
3108
|
+
`surface evidence missing: catalog package ${name} has no hosted guide`,
|
|
3109
|
+
),
|
|
3110
|
+
],
|
|
2586
3111
|
)
|
|
2587
|
-
|
|
2588
|
-
|
|
3112
|
+
// A guide without a Surface section answers its violation and no names.
|
|
3113
|
+
const guides = globSync('guides/*.md', { cwd: host })
|
|
3114
|
+
.map(normalizePolicyPath)
|
|
3115
|
+
.sort()
|
|
3116
|
+
.flatMap(
|
|
3117
|
+
(
|
|
3118
|
+
path,
|
|
3119
|
+
): ReadonlyArray<{
|
|
3120
|
+
readonly owner: string
|
|
3121
|
+
readonly symbols: readonly string[]
|
|
3122
|
+
readonly violation?: PolicyViolation
|
|
3123
|
+
}> => {
|
|
3124
|
+
const owner = basename(path, '.md')
|
|
3125
|
+
if (owner === POLICY_GUIDE_MAP) return []
|
|
3126
|
+
const guide = createGuide(readFileSync(join(host, path), 'utf8'))
|
|
3127
|
+
return guide.sections().includes('Surface')
|
|
3128
|
+
? [{ owner, symbols: guide.surface().map((symbol) => symbol.name) }]
|
|
3129
|
+
: [
|
|
3130
|
+
{
|
|
3131
|
+
owner,
|
|
3132
|
+
symbols: [],
|
|
3133
|
+
violation: createPolicyViolation(
|
|
3134
|
+
'surface',
|
|
3135
|
+
`${directory}/${owner}.md`,
|
|
3136
|
+
'surface evidence missing: hosted guide has no Surface section',
|
|
3137
|
+
),
|
|
3138
|
+
},
|
|
3139
|
+
]
|
|
3140
|
+
},
|
|
3141
|
+
)
|
|
3142
|
+
const grandfather = new Set(
|
|
3143
|
+
guides.filter((guide) => guide.owner === own).flatMap(({ symbols }) => symbols),
|
|
3144
|
+
)
|
|
3145
|
+
const claimed = Map.groupBy(
|
|
3146
|
+
guides
|
|
3147
|
+
.filter((guide) => guide.owner !== own)
|
|
3148
|
+
.flatMap(({ owner, symbols }) => symbols.map((name) => ({ name, owner }))),
|
|
3149
|
+
({ name }) => name,
|
|
3150
|
+
)
|
|
3151
|
+
const population = readPolicySurface(root)
|
|
3152
|
+
const setups = globSync('tests/setup*.ts', { cwd: root })
|
|
3153
|
+
.map(normalizePolicyPath)
|
|
3154
|
+
.sort()
|
|
3155
|
+
.flatMap((path) =>
|
|
2589
3156
|
path.endsWith('.test.ts') ||
|
|
2590
3157
|
HOST_PATHS.some(
|
|
2591
3158
|
(vendored) =>
|
|
2592
3159
|
path === normalizePolicyPath(vendored) ||
|
|
2593
3160
|
path.startsWith(`${normalizePolicyPath(vendored)}/`),
|
|
2594
3161
|
)
|
|
3162
|
+
? []
|
|
3163
|
+
: [collectPolicyDeclarations(root, path, readFileSync(join(root, path), 'utf8'))],
|
|
2595
3164
|
)
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
3165
|
+
const declarations = [
|
|
3166
|
+
...population.declarations.filter((declaration) => !grandfather.has(declaration.name)),
|
|
3167
|
+
...setups.flatMap((read) => read.declarations),
|
|
3168
|
+
]
|
|
3169
|
+
const claims = declarations.flatMap((declaration) =>
|
|
3170
|
+
[...new Set(claimed.get(declaration.name)?.map(({ owner }) => owner))].map((owner) => ({
|
|
3171
|
+
declaration,
|
|
3172
|
+
owner,
|
|
3173
|
+
key: `${declaration.path}\n${declaration.name}\n${owner}`,
|
|
3174
|
+
})),
|
|
3175
|
+
)
|
|
3176
|
+
const violations = [
|
|
3177
|
+
...missing,
|
|
3178
|
+
...guides.flatMap(({ violation }) => (violation === undefined ? [] : [violation])),
|
|
3179
|
+
...population.violations,
|
|
3180
|
+
...setups.flatMap((read) => (read.violation === undefined ? [] : [read.violation])),
|
|
3181
|
+
...claims
|
|
3182
|
+
.filter((claim, index) => claims.findIndex((other) => other.key === claim.key) === index)
|
|
3183
|
+
.map(({ declaration, owner }) =>
|
|
2608
3184
|
createPolicyViolation(
|
|
2609
3185
|
'surface',
|
|
2610
3186
|
declaration.path,
|
|
2611
3187
|
`surface name belongs to one package: ${declaration.name} (${owner})`,
|
|
2612
3188
|
declaration.line,
|
|
2613
3189
|
),
|
|
2614
|
-
)
|
|
2615
|
-
|
|
2616
|
-
}
|
|
3190
|
+
),
|
|
3191
|
+
]
|
|
2617
3192
|
return violations.sort((first, second) => {
|
|
2618
3193
|
if (first.path !== second.path) return first.path < second.path ? -1 : 1
|
|
2619
3194
|
if (first.line !== second.line) return (first.line ?? 0) - (second.line ?? 0)
|
|
@@ -3890,6 +4465,41 @@ export function collectSheets(directory: string): readonly string[] {
|
|
|
3890
4465
|
.sort()
|
|
3891
4466
|
}
|
|
3892
4467
|
|
|
4468
|
+
/**
|
|
4469
|
+
* Collects the study tests a workspace holds beneath `tests/studies`.
|
|
4470
|
+
* @param directory - The workspace root.
|
|
4471
|
+
* @returns Sorted workspace-relative paths of the physical `*.test.ts` files, nested or direct,
|
|
4472
|
+
* beneath a `tests/studies` directory whose stored name has exactly that case, outside any
|
|
4473
|
+
* `node_modules` or `.git` directory, which Vitest's default exclude never collects; empty when
|
|
4474
|
+
* the directory is absent, is not a directory, or holds none.
|
|
4475
|
+
*/
|
|
4476
|
+
export function collectStudies(directory: string): readonly string[] {
|
|
4477
|
+
const tests = resolve(directory, 'tests')
|
|
4478
|
+
if (!existsSync(tests)) return []
|
|
4479
|
+
// A directory entry carries the name the host stores, so matching it keeps the selection exact-case
|
|
4480
|
+
// on a host that folds case, and its type excludes a link without following it.
|
|
4481
|
+
const held = readdirSync(tests, { withFileTypes: true })
|
|
4482
|
+
if (!held.some((entry) => entry.name === 'studies' && entry.isDirectory())) return []
|
|
4483
|
+
const studies: string[] = []
|
|
4484
|
+
const pending = ['tests/studies']
|
|
4485
|
+
while (pending.length > 0) {
|
|
4486
|
+
const current = pending.pop() ?? ''
|
|
4487
|
+
for (const entry of readdirSync(resolve(directory, current), { withFileTypes: true })) {
|
|
4488
|
+
const path = `${current}/${entry.name}`
|
|
4489
|
+
// Vitest's default exclude collects nothing beneath these directories.
|
|
4490
|
+
if (entry.isDirectory()) {
|
|
4491
|
+
if (entry.name !== 'node_modules' && entry.name !== '.git') pending.push(path)
|
|
4492
|
+
} else if (
|
|
4493
|
+
entry.isFile() &&
|
|
4494
|
+
entry.name.endsWith('.test.ts') &&
|
|
4495
|
+
lstatSync(resolve(directory, path)).nlink === 1
|
|
4496
|
+
)
|
|
4497
|
+
studies.push(path)
|
|
4498
|
+
}
|
|
4499
|
+
}
|
|
4500
|
+
return studies.sort()
|
|
4501
|
+
}
|
|
4502
|
+
|
|
3893
4503
|
/**
|
|
3894
4504
|
* Collects occupied framework faces from the source and application trees.
|
|
3895
4505
|
* @param directory - The workspace root.
|
|
@@ -3954,20 +4564,51 @@ export function collectFaceWrappers(directory: string): readonly string[] {
|
|
|
3954
4564
|
export function inspectSheetConfiguration(project: unknown, script?: string): readonly string[] {
|
|
3955
4565
|
const test = readConfigRecord(readConfigRecord(project).test)
|
|
3956
4566
|
const setup = test.setupFiles
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
script !== undefined &&
|
|
4567
|
+
return [
|
|
4568
|
+
...['./tests/setup.ts', './tests/setupBrowser.ts', './tests/setupStyles.ts'].filter(
|
|
4569
|
+
(path) => !Array.isArray(setup) || !setup.includes(path),
|
|
4570
|
+
),
|
|
4571
|
+
...(test.isolate !== false ? ['isolate'] : []),
|
|
4572
|
+
...(script !== undefined &&
|
|
3963
4573
|
(!script.includes('vite.styles.config.ts') ||
|
|
3964
4574
|
!script.includes('vite.themes.config.ts') ||
|
|
3965
4575
|
script.indexOf('vite.styles.config.ts') > script.indexOf('vite.themes.config.ts'))
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
4576
|
+
? ['themes order']
|
|
4577
|
+
: []),
|
|
4578
|
+
]
|
|
3969
4579
|
}
|
|
3970
4580
|
|
|
4581
|
+
/** Lists artifact-loader paths and the computed ESM imports each boundary admits. */
|
|
4582
|
+
export const ARTIFACT_IMPORT_CASES: ReadonlyArray<readonly [string, boolean]> = Object.freeze([
|
|
4583
|
+
['src/server/artifacts/Artifacts.ts', true],
|
|
4584
|
+
['src/server/artifacts/nested/inspect.mts', true],
|
|
4585
|
+
['src/server/artifacts/inspect.js', true],
|
|
4586
|
+
['src/server/artifacts/inspect.mjs', true],
|
|
4587
|
+
['src/server/artifacts-lookalike/inspect.ts', false],
|
|
4588
|
+
['src/browser/artifacts/inspect.ts', false],
|
|
4589
|
+
['src/core/artifacts/inspect.ts', false],
|
|
4590
|
+
['app/server/artifacts/inspect.ts', false],
|
|
4591
|
+
['src/server/artifacts/inspect.cts', false],
|
|
4592
|
+
['src/server/artifacts/inspect.cjs', false],
|
|
4593
|
+
['tools/artifacts/inspect.ts', true],
|
|
4594
|
+
['tools/artifacts/nested/inspect.mts', true],
|
|
4595
|
+
['tools/artifacts/inspect.js', true],
|
|
4596
|
+
['tools/artifacts/inspect.mjs', true],
|
|
4597
|
+
['.orkestrel/sample/instruments/tools/artifacts/inspect.ts', true],
|
|
4598
|
+
['.orkestrel/sample/instruments/harness/tools/artifacts/nested/inspect.mts', true],
|
|
4599
|
+
['.orkestrel/sample/instruments/harness/tools/artifacts/inspect.js', true],
|
|
4600
|
+
['.orkestrel/sample/instruments/harness/tools/artifacts/inspect.mjs', true],
|
|
4601
|
+
['src/server/inspect.ts', false],
|
|
4602
|
+
['src/server/tools/artifacts/inspect.ts', false],
|
|
4603
|
+
['configs/inspect.ts', false],
|
|
4604
|
+
['scripts/inspect.ts', false],
|
|
4605
|
+
['.orkestrel/sample/instruments/harness/inspect.ts', false],
|
|
4606
|
+
['tools/artifacts-lookalike/inspect.ts', false],
|
|
4607
|
+
['.orkestrel/sample/instruments/harness/tools/artifacts-lookalike/inspect.ts', false],
|
|
4608
|
+
['tools/artifacts/inspect.cts', false],
|
|
4609
|
+
['tools/artifacts/inspect.cjs', false],
|
|
4610
|
+
])
|
|
4611
|
+
|
|
3971
4612
|
/**
|
|
3972
4613
|
* Reads import restrictions and collection sentinels from the installed linter.
|
|
3973
4614
|
* @param directory - The fixture workspace containing the lint configuration.
|
|
@@ -4003,18 +4644,92 @@ export function readImportDiagnostics(
|
|
|
4003
4644
|
)
|
|
4004
4645
|
const diagnostics = readConfigRecord(JSON.parse(result.stdout)).diagnostics
|
|
4005
4646
|
if (!Array.isArray(diagnostics)) throw new Error('Missing lint diagnostics')
|
|
4006
|
-
const codes = new Set
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4647
|
+
const codes = new Set(
|
|
4648
|
+
diagnostics.map((value: unknown) => {
|
|
4649
|
+
const diagnostic = readConfigRecord(value)
|
|
4650
|
+
if (typeof diagnostic.code !== 'string' || typeof diagnostic.filename !== 'string')
|
|
4651
|
+
throw new Error('Missing diagnostic location')
|
|
4652
|
+
return (
|
|
4653
|
+
diagnostic.code +
|
|
4013
4654
|
' ' +
|
|
4014
|
-
normalizePolicyFilename(realpathSync.native(directory), diagnostic.filename)
|
|
4015
|
-
|
|
4016
|
-
|
|
4655
|
+
normalizePolicyFilename(realpathSync.native(directory), diagnostic.filename)
|
|
4656
|
+
)
|
|
4657
|
+
}),
|
|
4658
|
+
)
|
|
4017
4659
|
for (const path of paths)
|
|
4018
4660
|
if (!codes.has('eslint(no-debugger) ' + path)) throw new Error(`Uncollected fixture ${path}`)
|
|
4019
4661
|
return codes
|
|
4020
4662
|
}
|
|
4663
|
+
|
|
4664
|
+
/**
|
|
4665
|
+
* Reports whether a path is a physical directory, never a link to one.
|
|
4666
|
+
*
|
|
4667
|
+
* @remarks
|
|
4668
|
+
* Reads the same two facts `isPhysicalDirectory` reads off an `lstat` and returns false for every
|
|
4669
|
+
* inspection error, because this module is vendored byte-identical and imports nothing from the
|
|
4670
|
+
* package.
|
|
4671
|
+
*
|
|
4672
|
+
* @param path - The absolute path to inspect.
|
|
4673
|
+
* @returns True if the path is a directory and not a symbolic link; false otherwise, an unreadable
|
|
4674
|
+
* path included.
|
|
4675
|
+
*/
|
|
4676
|
+
export function isPolicyDirectory(path: string): boolean {
|
|
4677
|
+
try {
|
|
4678
|
+
const entry = lstatSync(path)
|
|
4679
|
+
return entry.isDirectory() && !entry.isSymbolicLink()
|
|
4680
|
+
} catch {
|
|
4681
|
+
return false
|
|
4682
|
+
}
|
|
4683
|
+
}
|
|
4684
|
+
|
|
4685
|
+
/**
|
|
4686
|
+
* Resolves a module specifier from this module without throwing.
|
|
4687
|
+
*
|
|
4688
|
+
* @param specifier - The package or subpath specifier to resolve.
|
|
4689
|
+
* @returns The resolved file path, or `undefined` when the workspace installs no such package.
|
|
4690
|
+
*/
|
|
4691
|
+
export function resolvePolicyPackage(specifier: string): string | undefined {
|
|
4692
|
+
try {
|
|
4693
|
+
return createRequire(import.meta.url).resolve(specifier)
|
|
4694
|
+
} catch {
|
|
4695
|
+
return undefined
|
|
4696
|
+
}
|
|
4697
|
+
}
|
|
4698
|
+
|
|
4699
|
+
/** Names the workspace root, the directory above `tests/`, that the vendored proofs inspect. */
|
|
4700
|
+
export const POLICY_ROOT: string = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
|
4701
|
+
|
|
4702
|
+
/**
|
|
4703
|
+
* Holds whether the workspace publishes source from `src`; false for one with application environments alone.
|
|
4704
|
+
*
|
|
4705
|
+
* @remarks
|
|
4706
|
+
* The `src` axis a declaration roll-up reads, derived from a recognized source environment that is
|
|
4707
|
+
* a physical directory rather than from a `src` entry of any shape. A regular file, an empty
|
|
4708
|
+
* directory, a directory holding no recognized environment, a symbolic link, and an entry this host
|
|
4709
|
+
* refuses to inspect each leave a workspace publishing nothing, and reading the entry alone would
|
|
4710
|
+
* put every one of them under the face proof. The inspection is caught rather than excused by
|
|
4711
|
+
* option: `throwIfNoEntry` excuses an absent path alone and rethrows a permission refusal, and a
|
|
4712
|
+
* throw at module scope is a collection error that takes a whole file down in that target rather
|
|
4713
|
+
* than failing one case. This is the derivation `targetToEnvironments` uses, which filters the
|
|
4714
|
+
* environment names by a contained physical directory, and it answers the same question
|
|
4715
|
+
* `blueprintToScripts` answers from a blueprint's own `src` list. A workspace declaring app
|
|
4716
|
+
* environments alone publishes no library, so
|
|
4717
|
+
* it vendors no `configs/src/` face wrapper and has no roll-up to measure. A face proof is
|
|
4718
|
+
* conditioned on this axis rather than on a face wrapper: a workspace that declares the axis and
|
|
4719
|
+
* vendors no wrapper carries the defect such a proof reports, so that workspace must still fail the
|
|
4720
|
+
* proof. The manifest cases read the same fact from their own `private` flag and name it
|
|
4721
|
+
* `manifestPublishes` for the source that decides it there; this reads the axis on disk, so a
|
|
4722
|
+
* workspace whose manifest and shape disagree fails one of the two rather than neither.
|
|
4723
|
+
*/
|
|
4724
|
+
export const POLICY_PUBLISHES: boolean = ['core', 'browser', 'server'].some((environment) =>
|
|
4725
|
+
isPolicyDirectory(resolve(POLICY_ROOT, 'src', environment)),
|
|
4726
|
+
)
|
|
4727
|
+
|
|
4728
|
+
/**
|
|
4729
|
+
* Holds the declaration extractor's entry path, or `undefined` where the workspace installs none.
|
|
4730
|
+
*
|
|
4731
|
+
* @remarks
|
|
4732
|
+
* A declaration roll-up loads the extractor package, which only a workspace publishing source from
|
|
4733
|
+
* `src` installs. The resolution is the mechanism its proof is conditioned on.
|
|
4734
|
+
*/
|
|
4735
|
+
export const POLICY_EXTRACTOR: string | undefined = resolvePolicyPackage('@microsoft/api-extractor')
|