@orkestrel/scaffold 0.0.39 → 0.0.41

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.
Files changed (37) hide show
  1. package/dist/bin/main.js +8 -1
  2. package/dist/bin/main.js.map +1 -1
  3. package/dist/host/AGENTS.md +2 -2
  4. package/dist/host/agents/orchestration.md +33 -2
  5. package/dist/host/agents/skills/enterprise-bootstrap/references/bootstrap-reference.md +16 -16
  6. package/dist/host/agents/skills/enterprise-bootstrap/references/components.md +3 -3
  7. package/dist/host/agents/skills/orkestrel-align-packages/references/integration.md +1 -1
  8. package/dist/host/agents/skills/orkestrel-build-application/SKILL.md +7 -2
  9. package/dist/host/claude/agents/builder.md +2 -2
  10. package/dist/host/claude/agents/orkestrel.md +48 -48
  11. package/dist/host/claude/rules/application.md +6 -4
  12. package/dist/host/claude/rules/architecture.md +2 -0
  13. package/dist/host/claude/rules/documentation.md +6 -0
  14. package/dist/host/claude/rules/tests.md +11 -1
  15. package/dist/host/claude/rules/typescript.md +15 -1
  16. package/dist/host/claude/rules/workspace.md +43 -13
  17. package/dist/host/claude/rules/writing.md +125 -0
  18. package/dist/host/claude/skills/enterprise-bootstrap/SKILL.md +10 -1
  19. package/dist/host/claude/skills/orkestrel-align-packages/SKILL.md +1 -1
  20. package/dist/host/claude/skills/orkestrel-build-application/SKILL.md +1 -1
  21. package/dist/host/claude/skills/orkestrel-harden-package/SKILL.md +1 -1
  22. package/dist/host/configs/policy.ts +185 -0
  23. package/dist/host/dotfiles/oxlintrc.json +13 -1
  24. package/dist/host/dotfiles/prettierignore +3 -0
  25. package/dist/host/guides/scaffold.md +13 -7
  26. package/dist/host/manifest.json +11 -7
  27. package/dist/host/tests/config.test.ts +195 -4
  28. package/dist/host/tests/policy.test.ts +82 -0
  29. package/dist/host/tests/setupPolicy.ts +863 -21
  30. package/dist/src/core/index.cjs +68 -6
  31. package/dist/src/core/index.cjs.map +1 -1
  32. package/dist/src/core/index.d.cts +11 -9
  33. package/dist/src/core/index.d.ts +11 -9
  34. package/dist/src/core/index.js +68 -6
  35. package/dist/src/core/index.js.map +1 -1
  36. package/package.json +2 -2
  37. package/dist/host/agents/skills/orkestrel-build-application/references/application.md +0 -129
@@ -1,6 +1,7 @@
1
1
  // P1: Every checked population must exist and be non-empty; absence fails instead of passing vacuously.
2
2
  // P2: Required items are checked strictly; extra items are ignored before their shape is read.
3
3
 
4
+ import { spawnSync } from 'node:child_process'
4
5
  import {
5
6
  existsSync,
6
7
  globSync,
@@ -14,10 +15,12 @@ import {
14
15
  import { dirname, join, resolve } from 'node:path'
15
16
  import { fileURLToPath, pathToFileURL } from 'node:url'
16
17
  import { build, loadConfigFromFile } from 'vite'
17
- import { createScratch } from '@orkestrel/test/server'
18
+ import { RuleTester } from 'oxlint/plugins-dev'
18
19
  import * as configHelpers from '../configs/helpers.js'
20
+ import { MOCKING_RULE, PRIVACY_RULE } from '../configs/policy.js'
19
21
  import configuration, { resolveWorkspacePath } from '../vite.config.js'
20
22
  import tsconfig from '../tsconfig.json' with { type: 'json' }
23
+ import { createPolicyScratch, inspectPolicyConfiguration } from './setupPolicy.js'
21
24
  import { describe, expect, it } from 'vitest'
22
25
 
23
26
  const root = resolve(dirname(fileURLToPath(import.meta.url)), '..')
@@ -452,6 +455,194 @@ describe('root configuration', () => {
452
455
  hasService && publishes,
453
456
  )
454
457
  })
458
+
459
+ it('keeps policy rules active across every linted workspace path', () => {
460
+ const parsed: unknown = JSON.parse(readFileSync(resolve(root, '.oxlintrc.json'), 'utf8'))
461
+ expect(inspectPolicyConfiguration(parsed)).toEqual([])
462
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
463
+ throw new Error('The Oxlint configuration is not a record')
464
+ }
465
+ const controlled = structuredClone(parsed)
466
+ const overrides: unknown = Object.getOwnPropertyDescriptor(controlled, 'overrides')?.value
467
+ if (!Array.isArray(overrides)) throw new Error('The Oxlint configuration has no overrides')
468
+ Object.defineProperty(controlled, 'overrides', {
469
+ value: overrides.concat({
470
+ files: ['src/**'],
471
+ rules: { 'policy/no-mocking': 'off' },
472
+ }),
473
+ enumerable: true,
474
+ configurable: true,
475
+ writable: true,
476
+ })
477
+ expect(inspectPolicyConfiguration(controlled)).toEqual([
478
+ 'overrides must not configure policy/no-mocking',
479
+ ])
480
+ })
481
+
482
+ it('omits the audit-confirmed dead policy type exports', () => {
483
+ const source = readFileSync(resolve(root, 'configs/policy.ts'), 'utf8')
484
+ expect(source).not.toMatch(/\bPolicy(?:Call|ClassMember)\b/u)
485
+ })
486
+ })
487
+
488
+ describe('policy plugin', () => {
489
+ RuleTester.describe = describe
490
+ RuleTester.it = it
491
+
492
+ const tester = new RuleTester({ languageOptions: { parserOptions: { lang: 'ts' } } })
493
+ tester.run('no-mocking', MOCKING_RULE, {
494
+ valid: [
495
+ { name: 'accepts recorders', code: 'createRecorder()' },
496
+ { name: 'accepts non-framework members', code: "registry.mock('./x')" },
497
+ { name: 'accepts unlisted framework members', code: 'vi.clearAllMocks()' },
498
+ ],
499
+ invalid: [
500
+ {
501
+ name: 'rejects module mocking [membership: named vi and jest module APIs]',
502
+ code: "vi.mock('./x')",
503
+ errors: [{ messageId: 'mock' }],
504
+ },
505
+ {
506
+ name: 'rejects computed module mocking [membership: named vi and jest module APIs]',
507
+ code: `vi['mock']('./x')`,
508
+ errors: [{ messageId: 'mock' }],
509
+ },
510
+ {
511
+ name: 'rejects template module mocking [membership: named vi and jest module APIs]',
512
+ code: `vi[\`mock\`]('./x')`,
513
+ errors: [{ messageId: 'mock' }],
514
+ },
515
+ {
516
+ name: 'rejects spy factories [membership: named vi and jest spy APIs]',
517
+ code: 'jest.fn()',
518
+ errors: [{ messageId: 'spy' }],
519
+ },
520
+ {
521
+ name: 'rejects fake clocks [membership: named vi and jest clock APIs]',
522
+ code: 'vi.useFakeTimers()',
523
+ errors: [{ messageId: 'clock' }],
524
+ },
525
+ {
526
+ name: 'rejects environment stubs [membership: named vi and jest stub APIs]',
527
+ code: "vi.stubEnv('A', '1')",
528
+ errors: [{ messageId: 'stub' }],
529
+ },
530
+ ],
531
+ })
532
+
533
+ tester.run('no-keyword-privacy', PRIVACY_RULE, {
534
+ valid: [
535
+ { name: 'accepts runtime-private fields', code: 'class Example { #value = 1 }' },
536
+ {
537
+ name: 'accepts unannotated members',
538
+ code: 'class Example { value = 1; read() { return this.value } }',
539
+ },
540
+ ],
541
+ invalid: [
542
+ {
543
+ name: 'rejects private properties [membership: keyword-annotated class members]',
544
+ code: 'class Example { private value = 1 }',
545
+ errors: [{ messageId: 'keyword' }],
546
+ },
547
+ {
548
+ name: 'rejects private methods [membership: keyword-annotated class members]',
549
+ code: 'class Example { private read() { return 1 } }',
550
+ errors: [{ messageId: 'keyword' }],
551
+ },
552
+ {
553
+ name: 'rejects protected properties [membership: keyword-annotated class members]',
554
+ code: 'class Example { protected value = 1 }',
555
+ errors: [{ messageId: 'keyword' }],
556
+ },
557
+ {
558
+ name: 'rejects protected methods [membership: keyword-annotated class members]',
559
+ code: 'class Example { protected read() { return 1 } }',
560
+ errors: [{ messageId: 'keyword' }],
561
+ },
562
+ ],
563
+ })
564
+
565
+ it('loads every configured policy rule through the real binary', () => {
566
+ const scratch = createPolicyScratch({ prefix: 'orkestrel-config-policy-' })
567
+ try {
568
+ scratch.write(
569
+ 'violations/fixture.ts',
570
+ [
571
+ "vi.mock('./x')",
572
+ 'class PrivateMember { private value = 1 }',
573
+ 'class ParameterMember { constructor(readonly value: string) {} }',
574
+ 'class PublicMember { public value = 1 }',
575
+ 'void PrivateMember',
576
+ 'void ParameterMember',
577
+ 'void PublicMember',
578
+ ].join('\n'),
579
+ )
580
+ scratch.write(
581
+ 'clean/fixture.ts',
582
+ [
583
+ 'class CleanMember {',
584
+ '\t#value = 1',
585
+ '\tvalue(): number { return this.#value }',
586
+ '}',
587
+ 'void CleanMember',
588
+ ].join('\n'),
589
+ )
590
+
591
+ const binary = resolve(root, 'node_modules/.bin/oxlint')
592
+ const config = resolve(root, '.oxlintrc.json')
593
+ const violations = spawnSync(
594
+ binary,
595
+ ['--config', config, '--format', 'json', resolve(scratch.path, 'violations')],
596
+ { cwd: root, encoding: 'utf8', timeout: 15_000 },
597
+ )
598
+ const clean = spawnSync(
599
+ binary,
600
+ ['--config', config, '--format', 'json', resolve(scratch.path, 'clean')],
601
+ { cwd: root, encoding: 'utf8', timeout: 15_000 },
602
+ )
603
+ const reports: string[][] = []
604
+ for (const result of [violations, clean]) {
605
+ if (result.error !== undefined) throw result.error
606
+ const report: unknown = JSON.parse(result.stdout)
607
+ if (typeof report !== 'object' || report === null) {
608
+ throw new Error('Oxlint returned no JSON report')
609
+ }
610
+ const diagnostics: unknown = Object.getOwnPropertyDescriptor(report, 'diagnostics')?.value
611
+ if (!Array.isArray(diagnostics)) throw new Error('Oxlint returned no diagnostic list')
612
+ const codes: string[] = []
613
+ for (const diagnostic of diagnostics) {
614
+ if (typeof diagnostic !== 'object' || diagnostic === null) {
615
+ throw new Error('Oxlint returned a malformed diagnostic')
616
+ }
617
+ const code: unknown = Object.getOwnPropertyDescriptor(diagnostic, 'code')?.value
618
+ if (typeof code !== 'string') {
619
+ throw new Error('Oxlint returned a diagnostic without a rule id')
620
+ }
621
+ codes.push(code)
622
+ }
623
+ reports.push(codes)
624
+ }
625
+
626
+ const violationCodes = reports[0]
627
+ const cleanCodes = reports[1]
628
+ if (violationCodes === undefined || cleanCodes === undefined) {
629
+ throw new Error('Oxlint returned no fixture reports')
630
+ }
631
+ expect(violations.status).toBe(1)
632
+ for (const rule of [
633
+ 'policy(no-mocking)',
634
+ 'policy(no-keyword-privacy)',
635
+ 'typescript(parameter-properties)',
636
+ 'typescript(explicit-member-accessibility)',
637
+ ]) {
638
+ expect(violationCodes).toContain(rule)
639
+ }
640
+ expect(clean.status).toBe(0)
641
+ expect(cleanCodes).toHaveLength(0)
642
+ } finally {
643
+ scratch.destroy()
644
+ }
645
+ })
455
646
  })
456
647
 
457
648
  describe('configuration helpers', () => {
@@ -490,7 +681,7 @@ describe('configuration helpers', () => {
490
681
  })
491
682
 
492
683
  it('resolves contained workspace paths and refuses a real outside sibling', () => {
493
- const scratch = createScratch({ prefix: 'orkestrel-config-outside-' })
684
+ const scratch = createPolicyScratch({ prefix: 'orkestrel-config-outside-' })
494
685
  try {
495
686
  const outside = scratch.path
496
687
  const importer = resolve(root, 'tests/config.test.ts')
@@ -513,7 +704,7 @@ describe('configuration helpers', () => {
513
704
  })
514
705
 
515
706
  it('reads bounded files and resolves package roots from real manifests', () => {
516
- const scratch = createScratch({ prefix: 'orkestrel-config-package-' })
707
+ const scratch = createPolicyScratch({ prefix: 'orkestrel-config-package-' })
517
708
  try {
518
709
  const workspace = scratch.path
519
710
  const packageRoot = resolve(workspace, 'node_modules/@sample/package')
@@ -551,7 +742,7 @@ describe('configuration helpers', () => {
551
742
  })
552
743
 
553
744
  it('classifies module boundaries and extracts static asset sources', async () => {
554
- const scratch = createScratch({ prefix: 'orkestrel-config-assets-' })
745
+ const scratch = createPolicyScratch({ prefix: 'orkestrel-config-assets-' })
555
746
  try {
556
747
  const workspace = scratch.path
557
748
  const source = resolve(workspace, 'entry.ts')
@@ -1,5 +1,7 @@
1
1
  import { describe, expect, it } from 'vitest'
2
2
  import {
3
+ BRIDGE_POLICY_CONTROLS,
4
+ createPolicyScratch,
3
5
  FUNCTION_SOURCE_FILES,
4
6
  GENERIC_POLICY_SOURCES,
5
7
  inspectPolicyControl,
@@ -7,15 +9,36 @@ import {
7
9
  inspectPolicySources,
8
10
  inspectPolicyWorkspace,
9
11
  inspectSkillFamily,
12
+ inspectSkillBridges,
13
+ matchesSkillTrigger,
14
+ parseSkillFrontmatter,
10
15
  POLICY_CONTROLS,
16
+ POLICY_SUPPRESSION_DIRECTIVE,
11
17
  readSkillFamily,
12
18
  SKILL_POLICY_APOSTROPHE,
19
+ SKILL_POLICY_BACKTICKED,
13
20
  SKILL_POLICY_CONTROLS,
14
21
  SKILL_POLICY_EXCLUSION,
22
+ SKILL_POLICY_FOLDED,
23
+ SKILL_POLICY_PARAGRAPHS,
15
24
  stemToPolicyCandidates,
16
25
  testToPolicyStem,
17
26
  } from './setupPolicy.js'
18
27
 
28
+ describe('policy scratch', () => {
29
+ it('contains every write within its root', () => {
30
+ const scratch = createPolicyScratch({ prefix: 'orkestrel-policy-containment-' })
31
+ try {
32
+ expect(() => scratch.write('inside/fixture.ts', '')).not.toThrow()
33
+ expect(() => scratch.write('../escape', '')).toThrow(
34
+ 'Scratch target must stay within its root',
35
+ )
36
+ } finally {
37
+ scratch.destroy()
38
+ }
39
+ })
40
+ })
41
+
19
42
  describe('fleet policy register', () => {
20
43
  it('keeps handlers in the function set and routes out', () => {
21
44
  expect(FUNCTION_SOURCE_FILES).toContain('handlers.ts')
@@ -285,6 +308,22 @@ describe('policy population controls', () => {
285
308
  }),
286
309
  ).toEqual([])
287
310
  })
311
+
312
+ it('excludes documentation from the suppression population', () => {
313
+ expect(
314
+ inspectPolicyControl({
315
+ label: 'excludes documentation from the suppression population',
316
+ membership: 'files outside source, test, config, and script code',
317
+ rule: 'suppression',
318
+ files: [
319
+ {
320
+ path: 'guides/sample.md',
321
+ content: `<!-- ${POLICY_SUPPRESSION_DIRECTIVE} -->\n`,
322
+ },
323
+ ],
324
+ }),
325
+ ).toEqual([])
326
+ })
288
327
  })
289
328
 
290
329
  describe('instrument negative controls', () => {
@@ -307,11 +346,20 @@ describe('skill family policy', () => {
307
346
  expect(inspectSkillFamily(process.cwd())).toEqual([])
308
347
  })
309
348
 
349
+ it('parses a folded description containing a colon as exactly two frontmatter keys', () => {
350
+ const skill = SKILL_POLICY_FOLDED.files.find((file) => file.path.endsWith('/SKILL.md'))
351
+ const frontmatter = parseSkillFrontmatter(skill?.content ?? '')
352
+ expect(frontmatter?.keys).toEqual(['name', 'description'])
353
+ expect(frontmatter?.name).toBe('sample')
354
+ expect(frontmatter?.description).toBe('Use this skill when a continuation contains: a colon.')
355
+ })
356
+
310
357
  for (const control of SKILL_POLICY_CONTROLS) {
311
358
  it(`${control.label} [membership: ${control.membership}]`, () => {
312
359
  const violations = inspectPolicyControl(control)
313
360
  expect(violations).toHaveLength(1)
314
361
  expect(violations[0]?.rule).toBe(control.rule)
362
+ expect(control.message === undefined || violations[0]?.message === control.message).toBe(true)
315
363
  })
316
364
  }
317
365
 
@@ -319,11 +367,45 @@ describe('skill family policy', () => {
319
367
  expect(inspectPolicyControl(SKILL_POLICY_APOSTROPHE)).toEqual([])
320
368
  })
321
369
 
370
+ it(`${SKILL_POLICY_FOLDED.label} [membership: ${SKILL_POLICY_FOLDED.membership}]`, () => {
371
+ expect(inspectPolicyControl(SKILL_POLICY_FOLDED)).toEqual([])
372
+ })
373
+
374
+ it(`${SKILL_POLICY_BACKTICKED.label} [membership: ${SKILL_POLICY_BACKTICKED.membership}]`, () => {
375
+ expect(inspectPolicyControl(SKILL_POLICY_BACKTICKED)).toEqual([])
376
+ })
377
+
378
+ it('parses a folded description containing two paragraphs', () => {
379
+ const skill = SKILL_POLICY_PARAGRAPHS.files.find((file) => file.path.endsWith('/SKILL.md'))
380
+ const frontmatter = parseSkillFrontmatter(skill?.content ?? '')
381
+ expect(frontmatter?.keys).toEqual(['name', 'description'])
382
+ expect(frontmatter?.description).toBe(
383
+ 'First paragraph.\nUse `--app` when a policy fixture needs it.',
384
+ )
385
+ expect(matchesSkillTrigger(frontmatter?.description ?? '')).toBe(true)
386
+ expect(inspectPolicyControl(SKILL_POLICY_PARAGRAPHS)).toEqual([])
387
+ })
388
+
322
389
  it(`${SKILL_POLICY_EXCLUSION.label} [membership: ${SKILL_POLICY_EXCLUSION.membership}]`, () => {
323
390
  expect(inspectPolicyControl(SKILL_POLICY_EXCLUSION)).toEqual([])
324
391
  })
325
392
  })
326
393
 
394
+ describe('skill bridge policy', () => {
395
+ it('matches every real provider bridge to its canonical skill', () => {
396
+ expect(inspectSkillBridges(process.cwd())).toEqual([])
397
+ })
398
+
399
+ for (const control of BRIDGE_POLICY_CONTROLS) {
400
+ it(`${control.label} [membership: ${control.membership}]`, () => {
401
+ const violations = inspectPolicyControl(control)
402
+ expect(violations).toHaveLength(1)
403
+ expect(violations[0]?.rule).toBe(control.rule)
404
+ expect(control.message === undefined || violations[0]?.message === control.message).toBe(true)
405
+ })
406
+ }
407
+ })
408
+
327
409
  describe('repository policy', () => {
328
410
  it('enforces placement and mirrors over the real workspace', () => {
329
411
  expect(inspectPolicyWorkspace(process.cwd())).toEqual([])