@orkestrel/scaffold 0.0.48 → 0.0.49

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.
@@ -15,7 +15,7 @@ import {
15
15
  import { createRequire } from 'node:module'
16
16
  import { dirname, join, resolve } from 'node:path'
17
17
  import { fileURLToPath, pathToFileURL } from 'node:url'
18
- import { build, loadConfigFromFile } from 'vite'
18
+ import { build, createServer, loadConfigFromFile } from 'vite'
19
19
  import { RuleTester } from 'oxlint/plugins-dev'
20
20
  import * as configHelpers from '../configs/helpers.js'
21
21
  import { MOCKING_RULE, NESTED_RULE, PRIVACY_RULE } from '../configs/policy.js'
@@ -513,6 +513,108 @@ describe('root configuration', () => {
513
513
  )
514
514
  })
515
515
 
516
+ it('keeps the committed host inventory aligned with the vendored checkout bytes', async () => {
517
+ // Run this gate against a quiescent checkout. It compares two reads and cannot
518
+ // distinguish stale committed data from a source edit made while it runs.
519
+ const packageValue: unknown = JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8'))
520
+ if (typeof packageValue !== 'object' || packageValue === null) {
521
+ throw new Error('The package manifest is not a record')
522
+ }
523
+ const scripts: unknown = Object.getOwnPropertyDescriptor(packageValue, 'scripts')?.value
524
+ if (typeof scripts !== 'object' || scripts === null) {
525
+ throw new Error('The package manifest carries no scripts')
526
+ }
527
+ const generator: unknown = Object.getOwnPropertyDescriptor(scripts, 'build:inventory')?.value
528
+ expect(generator === undefined || typeof generator === 'string').toBe(true)
529
+ if (generator === undefined) {
530
+ if (existsSync(resolve(root, 'host.json'))) {
531
+ throw new Error('A committed host inventory exists without a generator')
532
+ }
533
+ return
534
+ }
535
+ if (typeof generator !== 'string') {
536
+ throw new Error('The host inventory generator is not a script')
537
+ }
538
+ const committed = resolve(root, 'host.json')
539
+ if (!existsSync(committed)) {
540
+ throw new Error('The committed host inventory is absent at host.json')
541
+ }
542
+ const helper = resolve(root, 'src/server/helpers.ts')
543
+ if (!existsSync(helper)) {
544
+ throw new Error('The committed host inventory has no server stager')
545
+ }
546
+ const server = await createServer({
547
+ configFile: false,
548
+ root,
549
+ ...(configuration.resolve === undefined ? {} : { resolve: configuration.resolve }),
550
+ server: { middlewareMode: true },
551
+ })
552
+ try {
553
+ const loaded: unknown = await server.ssrLoadModule(helper)
554
+ if (typeof loaded !== 'object' || loaded === null) {
555
+ throw new Error('The server helper module did not load')
556
+ }
557
+ const stage: unknown = Reflect.get(loaded, 'stageInventory')
558
+ if (typeof stage !== 'function') {
559
+ throw new Error('The server helper module exports no stageInventory function')
560
+ }
561
+ const workspace = mkdtempSync(join(root, 'host-inventory-'))
562
+ try {
563
+ const generated = join(workspace, 'host.json')
564
+ Reflect.apply(stage, undefined, [root, generated])
565
+ const generatedText = readFileSync(generated, 'utf8')
566
+ const committedText = readFileSync(committed, 'utf8')
567
+ const values: readonly unknown[] = [JSON.parse(generatedText), JSON.parse(committedText)]
568
+ const indexes: Array<Map<string, string>> = []
569
+ for (const value of values) {
570
+ if (typeof value !== 'object' || value === null) {
571
+ throw new Error('A host inventory is not a record')
572
+ }
573
+ const entries: unknown = Object.getOwnPropertyDescriptor(value, 'entries')?.value
574
+ if (!Array.isArray(entries)) throw new Error('A host inventory carries no entry list')
575
+ const index = new Map<string, string>()
576
+ for (const entry of entries) {
577
+ if (typeof entry !== 'object' || entry === null) {
578
+ throw new Error('A host inventory carries a malformed entry')
579
+ }
580
+ const destination: unknown = Object.getOwnPropertyDescriptor(
581
+ entry,
582
+ 'destination',
583
+ )?.value
584
+ const digest: unknown = Object.getOwnPropertyDescriptor(entry, 'digest')?.value
585
+ if (typeof destination !== 'string' || typeof digest !== 'string') {
586
+ throw new Error('A host inventory entry carries no destination digest')
587
+ }
588
+ index.set(destination, digest)
589
+ }
590
+ indexes.push(index)
591
+ }
592
+ const generatedIndex = indexes[0]
593
+ const committedIndex = indexes[1]
594
+ if (generatedIndex === undefined || committedIndex === undefined) {
595
+ throw new Error('The host inventories were not indexed')
596
+ }
597
+ if (generatedText !== committedText) {
598
+ const stale: string[] = []
599
+ for (const [destination, digest] of generatedIndex) {
600
+ if (committedIndex.get(destination) !== digest) stale.push(destination)
601
+ }
602
+ for (const destination of committedIndex.keys()) {
603
+ if (!generatedIndex.has(destination)) stale.push(destination)
604
+ }
605
+ throw new Error(
606
+ `The committed host inventory is stale at ${stale.length > 0 ? stale.sort().join(', ') : 'host.json'}`,
607
+ )
608
+ }
609
+ console.info(`host-inventory: entries=${generatedIndex.size}`)
610
+ } finally {
611
+ rmSync(workspace, { recursive: true, force: true })
612
+ }
613
+ } finally {
614
+ await server.close()
615
+ }
616
+ })
617
+
516
618
  it('keeps policy rules active across every linted workspace path', () => {
517
619
  const parsed: unknown = JSON.parse(readFileSync(resolve(root, '.oxlintrc.json'), 'utf8'))
518
620
  expect(inspectPolicyConfiguration(parsed)).toEqual([])
@@ -4,7 +4,7 @@ let _orkestrel_template = require("@orkestrel/template");
4
4
  let _orkestrel_emitter = require("@orkestrel/emitter");
5
5
  var package_default = {
6
6
  name: "@orkestrel/scaffold",
7
- version: "0.0.48",
7
+ version: "0.0.49",
8
8
  description: "Scaffold workspaces with five commands: new, audit, repair, catalog, and overwrite.",
9
9
  keywords: [
10
10
  "audit",
@@ -84,6 +84,7 @@ var package_default = {
84
84
  "build:src:server": "vite build --config configs/src/vite.server.config.ts && npm run copy dist/src/server/index.d.ts dist/src/server/index.d.cts",
85
85
  "build:src:bin": "vite build --config configs/src/vite.bin.config.ts",
86
86
  "build:host": "node -e \"import('./dist/src/server/index.js').then((m)=>{const n=m.stageHost(process.cwd(),'dist/host').length;console.log('build-host: staged '+n+' file(s) into dist/host')})\"",
87
+ "build:inventory": "node -e \"import('./dist/src/server/index.js').then((m)=>{const p=process.argv[1]??'host.json',n=m.stageInventory(process.cwd(),p).entries.length;console.log('build-inventory: staged '+n+' file(s) into '+p)})\"",
87
88
  "prepack": "npm run build",
88
89
  "prepublishOnly": "npm run format:check && npm run lint:check && npm run check && npm run build && npm test && npm run test:distribution -- --mode release"
89
90
  },
@@ -100,7 +101,7 @@ var package_default = {
100
101
  "@orkestrel/guide": "^0.0.12",
101
102
  "@orkestrel/html": "^0.0.4",
102
103
  "@orkestrel/probe": "^0.0.2",
103
- "@orkestrel/test": "^0.0.9",
104
+ "@orkestrel/test": "^0.0.10",
104
105
  "@types/node": "^26.2.0",
105
106
  "@vitest/browser-playwright": "^4.1.11",
106
107
  "oxfmt": "^0.64.0",
@@ -252,6 +253,8 @@ var HOST_PATHS = Object.freeze([
252
253
  "guides/guide.md",
253
254
  "guides/scaffold.md"
254
255
  ]);
256
+ /** The repository-relative path where the committed vendored-file inventory is served. */
257
+ var HOST_INVENTORY_PATH = "host.json";
255
258
  /**
256
259
  * The vendored paths whose present bytes belong to each workspace, frozen.
257
260
  *
@@ -2062,7 +2065,7 @@ var isMirror = (0, _orkestrel_contract.unionOf)((0, _orkestrel_contract.recordOf
2062
2065
  }, ["observed"]), (0, _orkestrel_contract.recordOf)({
2063
2066
  name: isDependencyName,
2064
2067
  path: isPath,
2065
- lookup: (0, _orkestrel_contract.literalOf)("missing", "failed"),
2068
+ lookup: (0, _orkestrel_contract.literalOf)("missing", "unmatched", "failed"),
2066
2069
  note: _orkestrel_contract.isString,
2067
2070
  observed: isHex
2068
2071
  }, ["observed"]));
@@ -2080,7 +2083,7 @@ var isCatalogEntry = (0, _orkestrel_contract.unionOf)((0, _orkestrel_contract.re
2080
2083
  dependencies: (0, _orkestrel_contract.andOf)(isCollection, (0, _orkestrel_contract.arrayOf)(isDependency))
2081
2084
  }), (0, _orkestrel_contract.recordOf)({
2082
2085
  name: isDependencyName,
2083
- lookup: (0, _orkestrel_contract.literalOf)("missing", "failed"),
2086
+ lookup: (0, _orkestrel_contract.literalOf)("missing", "unmatched", "failed"),
2084
2087
  note: _orkestrel_contract.isString
2085
2088
  }));
2086
2089
  /**
@@ -2411,6 +2414,29 @@ function matchesOrchestrationPath(path) {
2411
2414
  return ORCHESTRATION_PATH_PREFIXES.some((prefix) => path.startsWith(prefix));
2412
2415
  }
2413
2416
  /**
2417
+ * Checks whether another surface owns the vendored bytes at a path.
2418
+ *
2419
+ * @param path - The target-relative vendored path to test.
2420
+ * @returns `true` for the catalog agent file and for a Markdown guide mirror;
2421
+ * `false` otherwise.
2422
+ *
2423
+ * @remarks
2424
+ * The materializer keeps these paths presence-owned because the catalog or
2425
+ * mirror surface writes their bytes. A live host fill therefore keeps the
2426
+ * installed floor's bytes for them and requests only the host-owned paths.
2427
+ *
2428
+ * @example
2429
+ * ```ts
2430
+ * import { isDeferredPath } from '@orkestrel/scaffold'
2431
+ *
2432
+ * isDeferredPath('guides/router.md') // true
2433
+ * isDeferredPath('AGENTS.md') // false
2434
+ * ```
2435
+ */
2436
+ function isDeferredPath(path) {
2437
+ return path === ".claude/agents/orkestrel.md" || path.startsWith("guides/") && path.endsWith(".md");
2438
+ }
2439
+ /**
2414
2440
  * Infer the {@link Group} a path belongs to.
2415
2441
  *
2416
2442
  * @param path - The target-relative path to classify.
@@ -5263,6 +5289,7 @@ exports.GLOBAL_SETUP_PATH = GLOBAL_SETUP_PATH;
5263
5289
  exports.GROUPS = GROUPS;
5264
5290
  exports.GUIDES_TEST_PATH = GUIDES_TEST_PATH;
5265
5291
  exports.HEX_PATTERN = HEX_PATTERN;
5292
+ exports.HOST_INVENTORY_PATH = HOST_INVENTORY_PATH;
5266
5293
  exports.HOST_PATHS = HOST_PATHS;
5267
5294
  exports.INTEGRATION_TEST_PATH = INTEGRATION_TEST_PATH;
5268
5295
  exports.INVALID_PATH_CHARACTER_PATTERN = INVALID_PATH_CHARACTER_PATTERN;
@@ -5333,6 +5360,7 @@ exports.isCollection = isCollection;
5333
5360
  exports.isCompilerHooks = isCompilerHooks;
5334
5361
  exports.isCompilerOptions = isCompilerOptions;
5335
5362
  exports.isContent = isContent;
5363
+ exports.isDeferredPath = isDeferredPath;
5336
5364
  exports.isDependency = isDependency;
5337
5365
  exports.isDependencyName = isDependencyName;
5338
5366
  exports.isEnvironment = isEnvironment;