@orkestrel/scaffold 0.0.40 → 0.0.42

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 (40) 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/skills/enterprise-bootstrap/references/bootstrap-reference.md +16 -16
  5. package/dist/host/agents/skills/enterprise-bootstrap/references/components.md +3 -3
  6. package/dist/host/agents/skills/orkestrel-align-packages/references/integration.md +1 -1
  7. package/dist/host/agents/skills/orkestrel-build-application/SKILL.md +7 -2
  8. package/dist/host/claude/agents/builder.md +2 -2
  9. package/dist/host/claude/agents/orkestrel.md +48 -48
  10. package/dist/host/claude/rules/application.md +6 -4
  11. package/dist/host/claude/rules/architecture.md +2 -0
  12. package/dist/host/claude/rules/documentation.md +6 -0
  13. package/dist/host/claude/rules/tests.md +11 -1
  14. package/dist/host/claude/rules/typescript.md +15 -1
  15. package/dist/host/claude/rules/workspace.md +43 -13
  16. package/dist/host/claude/rules/writing.md +125 -0
  17. package/dist/host/claude/skills/enterprise-bootstrap/SKILL.md +10 -1
  18. package/dist/host/claude/skills/orkestrel-align-packages/SKILL.md +1 -1
  19. package/dist/host/claude/skills/orkestrel-build-application/SKILL.md +1 -1
  20. package/dist/host/claude/skills/orkestrel-harden-package/SKILL.md +1 -1
  21. package/dist/host/configs/policy.ts +185 -0
  22. package/dist/host/dotfiles/oxlintrc.json +13 -1
  23. package/dist/host/dotfiles/prettierignore +3 -0
  24. package/dist/host/guides/scaffold.md +23 -10
  25. package/dist/host/manifest.json +11 -7
  26. package/dist/host/scripts/codex.sh +0 -0
  27. package/dist/host/scripts/cursor.sh +0 -0
  28. package/dist/host/scripts/deps.sh +0 -0
  29. package/dist/host/scripts/ollama.sh +0 -0
  30. package/dist/host/tests/config.test.ts +217 -4
  31. package/dist/host/tests/policy.test.ts +82 -0
  32. package/dist/host/tests/setupPolicy.ts +863 -21
  33. package/dist/src/core/index.cjs +97 -22
  34. package/dist/src/core/index.cjs.map +1 -1
  35. package/dist/src/core/index.d.cts +45 -29
  36. package/dist/src/core/index.d.ts +45 -29
  37. package/dist/src/core/index.js +96 -22
  38. package/dist/src/core/index.js.map +1 -1
  39. package/package.json +7 -7
  40. 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,
@@ -11,13 +12,16 @@ import {
11
12
  rmSync,
12
13
  writeFileSync,
13
14
  } from 'node:fs'
15
+ import { createRequire } from 'node:module'
14
16
  import { dirname, join, resolve } from 'node:path'
15
17
  import { fileURLToPath, pathToFileURL } from 'node:url'
16
18
  import { build, loadConfigFromFile } from 'vite'
17
- import { createScratch } from '@orkestrel/test/server'
19
+ import { RuleTester } from 'oxlint/plugins-dev'
18
20
  import * as configHelpers from '../configs/helpers.js'
21
+ import { MOCKING_RULE, PRIVACY_RULE } from '../configs/policy.js'
19
22
  import configuration, { resolveWorkspacePath } from '../vite.config.js'
20
23
  import tsconfig from '../tsconfig.json' with { type: 'json' }
24
+ import { createPolicyScratch, inspectPolicyConfiguration } from './setupPolicy.js'
21
25
  import { describe, expect, it } from 'vitest'
22
26
 
23
27
  const root = resolve(dirname(fileURLToPath(import.meta.url)), '..')
@@ -452,6 +456,215 @@ describe('root configuration', () => {
452
456
  hasService && publishes,
453
457
  )
454
458
  })
459
+
460
+ it('keeps policy rules active across every linted workspace path', () => {
461
+ const parsed: unknown = JSON.parse(readFileSync(resolve(root, '.oxlintrc.json'), 'utf8'))
462
+ expect(inspectPolicyConfiguration(parsed)).toEqual([])
463
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
464
+ throw new Error('The Oxlint configuration is not a record')
465
+ }
466
+ const controlled = structuredClone(parsed)
467
+ const overrides: unknown = Object.getOwnPropertyDescriptor(controlled, 'overrides')?.value
468
+ if (!Array.isArray(overrides)) throw new Error('The Oxlint configuration has no overrides')
469
+ Object.defineProperty(controlled, 'overrides', {
470
+ value: overrides.concat({
471
+ files: ['src/**'],
472
+ rules: { 'policy/no-mocking': 'off' },
473
+ }),
474
+ enumerable: true,
475
+ configurable: true,
476
+ writable: true,
477
+ })
478
+ expect(inspectPolicyConfiguration(controlled)).toEqual([
479
+ 'overrides must not configure policy/no-mocking',
480
+ ])
481
+ })
482
+
483
+ it('omits the audit-confirmed dead policy type exports', () => {
484
+ const source = readFileSync(resolve(root, 'configs/policy.ts'), 'utf8')
485
+ expect(source).not.toMatch(/\bPolicy(?:Call|ClassMember)\b/u)
486
+ })
487
+ })
488
+
489
+ describe('policy plugin', () => {
490
+ RuleTester.describe = describe
491
+ RuleTester.it = it
492
+
493
+ const tester = new RuleTester({ languageOptions: { parserOptions: { lang: 'ts' } } })
494
+ tester.run('no-mocking', MOCKING_RULE, {
495
+ valid: [
496
+ { name: 'accepts recorders', code: 'createRecorder()' },
497
+ { name: 'accepts non-framework members', code: "registry.mock('./x')" },
498
+ { name: 'accepts unlisted framework members', code: 'vi.clearAllMocks()' },
499
+ ],
500
+ invalid: [
501
+ {
502
+ name: 'rejects module mocking [membership: named vi and jest module APIs]',
503
+ code: "vi.mock('./x')",
504
+ errors: [{ messageId: 'mock' }],
505
+ },
506
+ {
507
+ name: 'rejects computed module mocking [membership: named vi and jest module APIs]',
508
+ code: `vi['mock']('./x')`,
509
+ errors: [{ messageId: 'mock' }],
510
+ },
511
+ {
512
+ name: 'rejects template module mocking [membership: named vi and jest module APIs]',
513
+ code: `vi[\`mock\`]('./x')`,
514
+ errors: [{ messageId: 'mock' }],
515
+ },
516
+ {
517
+ name: 'rejects spy factories [membership: named vi and jest spy APIs]',
518
+ code: 'jest.fn()',
519
+ errors: [{ messageId: 'spy' }],
520
+ },
521
+ {
522
+ name: 'rejects fake clocks [membership: named vi and jest clock APIs]',
523
+ code: 'vi.useFakeTimers()',
524
+ errors: [{ messageId: 'clock' }],
525
+ },
526
+ {
527
+ name: 'rejects environment stubs [membership: named vi and jest stub APIs]',
528
+ code: "vi.stubEnv('A', '1')",
529
+ errors: [{ messageId: 'stub' }],
530
+ },
531
+ ],
532
+ })
533
+
534
+ tester.run('no-keyword-privacy', PRIVACY_RULE, {
535
+ valid: [
536
+ { name: 'accepts runtime-private fields', code: 'class Example { #value = 1 }' },
537
+ {
538
+ name: 'accepts unannotated members',
539
+ code: 'class Example { value = 1; read() { return this.value } }',
540
+ },
541
+ ],
542
+ invalid: [
543
+ {
544
+ name: 'rejects private properties [membership: keyword-annotated class members]',
545
+ code: 'class Example { private value = 1 }',
546
+ errors: [{ messageId: 'keyword' }],
547
+ },
548
+ {
549
+ name: 'rejects private methods [membership: keyword-annotated class members]',
550
+ code: 'class Example { private read() { return 1 } }',
551
+ errors: [{ messageId: 'keyword' }],
552
+ },
553
+ {
554
+ name: 'rejects protected properties [membership: keyword-annotated class members]',
555
+ code: 'class Example { protected value = 1 }',
556
+ errors: [{ messageId: 'keyword' }],
557
+ },
558
+ {
559
+ name: 'rejects protected methods [membership: keyword-annotated class members]',
560
+ code: 'class Example { protected read() { return 1 } }',
561
+ errors: [{ messageId: 'keyword' }],
562
+ },
563
+ ],
564
+ })
565
+
566
+ it('loads every configured policy rule through the real binary', () => {
567
+ const scratch = createPolicyScratch({ prefix: 'orkestrel-config-policy-' })
568
+ try {
569
+ scratch.write(
570
+ 'violations/fixture.ts',
571
+ [
572
+ "vi.mock('./x')",
573
+ 'class PrivateMember { private value = 1 }',
574
+ 'class ParameterMember { constructor(readonly value: string) {} }',
575
+ 'class PublicMember { public value = 1 }',
576
+ 'void PrivateMember',
577
+ 'void ParameterMember',
578
+ 'void PublicMember',
579
+ ].join('\n'),
580
+ )
581
+ scratch.write(
582
+ 'clean/fixture.ts',
583
+ [
584
+ 'class CleanMember {',
585
+ '\t#value = 1',
586
+ '\tvalue(): number { return this.#value }',
587
+ '}',
588
+ 'void CleanMember',
589
+ ].join('\n'),
590
+ )
591
+
592
+ // Run oxlint's real Node entry through the current interpreter rather than the
593
+ // `node_modules/.bin/oxlint` shim. That shim is a POSIX `sh` script — a symlink to one on
594
+ // Linux, a `.cmd`/`.ps1` pair on Windows — and Windows `CreateProcess` cannot execute the
595
+ // extensionless form; spawning the `.cmd` would need `shell: true`, which breaks on paths
596
+ // containing spaces. Resolving through `createRequire` reads oxlint's own `bin` field, so
597
+ // the entry survives hoisting, a nested `node_modules` layout, and a future rename.
598
+ const manifestPath = createRequire(join(root, 'package.json')).resolve('oxlint/package.json')
599
+ const manifest: unknown = JSON.parse(readFileSync(manifestPath, 'utf8'))
600
+ if (typeof manifest !== 'object' || manifest === null) {
601
+ throw new Error('The oxlint package manifest is not an object')
602
+ }
603
+ const bin: unknown = Object.getOwnPropertyDescriptor(manifest, 'bin')?.value
604
+ const entry: unknown =
605
+ typeof bin === 'string'
606
+ ? bin
607
+ : typeof bin === 'object' && bin !== null
608
+ ? Object.getOwnPropertyDescriptor(bin, 'oxlint')?.value
609
+ : undefined
610
+ if (typeof entry !== 'string') {
611
+ throw new Error('The oxlint package declares no bin.oxlint entry')
612
+ }
613
+ const binary = resolve(dirname(manifestPath), entry)
614
+ const config = resolve(root, '.oxlintrc.json')
615
+ const violations = spawnSync(
616
+ process.execPath,
617
+ [binary, '--config', config, '--format', 'json', resolve(scratch.path, 'violations')],
618
+ { cwd: root, encoding: 'utf8', timeout: 15_000 },
619
+ )
620
+ const clean = spawnSync(
621
+ process.execPath,
622
+ [binary, '--config', config, '--format', 'json', resolve(scratch.path, 'clean')],
623
+ { cwd: root, encoding: 'utf8', timeout: 15_000 },
624
+ )
625
+ const reports: string[][] = []
626
+ for (const result of [violations, clean]) {
627
+ if (result.error !== undefined) throw result.error
628
+ const report: unknown = JSON.parse(result.stdout)
629
+ if (typeof report !== 'object' || report === null) {
630
+ throw new Error('Oxlint returned no JSON report')
631
+ }
632
+ const diagnostics: unknown = Object.getOwnPropertyDescriptor(report, 'diagnostics')?.value
633
+ if (!Array.isArray(diagnostics)) throw new Error('Oxlint returned no diagnostic list')
634
+ const codes: string[] = []
635
+ for (const diagnostic of diagnostics) {
636
+ if (typeof diagnostic !== 'object' || diagnostic === null) {
637
+ throw new Error('Oxlint returned a malformed diagnostic')
638
+ }
639
+ const code: unknown = Object.getOwnPropertyDescriptor(diagnostic, 'code')?.value
640
+ if (typeof code !== 'string') {
641
+ throw new Error('Oxlint returned a diagnostic without a rule id')
642
+ }
643
+ codes.push(code)
644
+ }
645
+ reports.push(codes)
646
+ }
647
+
648
+ const violationCodes = reports[0]
649
+ const cleanCodes = reports[1]
650
+ if (violationCodes === undefined || cleanCodes === undefined) {
651
+ throw new Error('Oxlint returned no fixture reports')
652
+ }
653
+ expect(violations.status).toBe(1)
654
+ for (const rule of [
655
+ 'policy(no-mocking)',
656
+ 'policy(no-keyword-privacy)',
657
+ 'typescript(parameter-properties)',
658
+ 'typescript(explicit-member-accessibility)',
659
+ ]) {
660
+ expect(violationCodes).toContain(rule)
661
+ }
662
+ expect(clean.status).toBe(0)
663
+ expect(cleanCodes).toHaveLength(0)
664
+ } finally {
665
+ scratch.destroy()
666
+ }
667
+ })
455
668
  })
456
669
 
457
670
  describe('configuration helpers', () => {
@@ -490,7 +703,7 @@ describe('configuration helpers', () => {
490
703
  })
491
704
 
492
705
  it('resolves contained workspace paths and refuses a real outside sibling', () => {
493
- const scratch = createScratch({ prefix: 'orkestrel-config-outside-' })
706
+ const scratch = createPolicyScratch({ prefix: 'orkestrel-config-outside-' })
494
707
  try {
495
708
  const outside = scratch.path
496
709
  const importer = resolve(root, 'tests/config.test.ts')
@@ -513,7 +726,7 @@ describe('configuration helpers', () => {
513
726
  })
514
727
 
515
728
  it('reads bounded files and resolves package roots from real manifests', () => {
516
- const scratch = createScratch({ prefix: 'orkestrel-config-package-' })
729
+ const scratch = createPolicyScratch({ prefix: 'orkestrel-config-package-' })
517
730
  try {
518
731
  const workspace = scratch.path
519
732
  const packageRoot = resolve(workspace, 'node_modules/@sample/package')
@@ -551,7 +764,7 @@ describe('configuration helpers', () => {
551
764
  })
552
765
 
553
766
  it('classifies module boundaries and extracts static asset sources', async () => {
554
- const scratch = createScratch({ prefix: 'orkestrel-config-assets-' })
767
+ const scratch = createPolicyScratch({ prefix: 'orkestrel-config-assets-' })
555
768
  try {
556
769
  const workspace = scratch.path
557
770
  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([])