@orkestrel/scaffold 0.0.22 → 0.0.23

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.
@@ -35,15 +35,15 @@ descriptions never enter agent instruction context.
35
35
  | Package | Version |
36
36
  | --------------------- | ------- |
37
37
  | @orkestrel/abort | 0.0.5 |
38
- | @orkestrel/agent | 0.0.13 |
38
+ | @orkestrel/agent | 0.0.14 |
39
39
  | @orkestrel/browser | 0.0.8 |
40
40
  | @orkestrel/budget | 0.0.5 |
41
41
  | @orkestrel/console | 0.0.4 |
42
- | @orkestrel/contract | 0.0.9 |
42
+ | @orkestrel/contract | 0.0.10 |
43
43
  | @orkestrel/csv | 0.0.2 |
44
44
  | @orkestrel/database | 0.0.7 |
45
45
  | @orkestrel/emitter | 0.0.5 |
46
- | @orkestrel/guide | 0.0.8 |
46
+ | @orkestrel/guide | 0.0.9 |
47
47
  | @orkestrel/html | 0.0.2 |
48
48
  | @orkestrel/indexeddb | 0.0.6 |
49
49
  | @orkestrel/interpret | 0.0.7 |
@@ -61,7 +61,7 @@ descriptions never enter agent instruction context.
61
61
  | @orkestrel/reason | 0.0.4 |
62
62
  | @orkestrel/relation | 0.0.7 |
63
63
  | @orkestrel/router | 0.0.8 |
64
- | @orkestrel/scaffold | 0.0.21 |
64
+ | @orkestrel/scaffold | 0.0.22 |
65
65
  | @orkestrel/sea | 0.0.5 |
66
66
  | @orkestrel/server | 0.0.10 |
67
67
  | @orkestrel/sqlite | 0.0.6 |
@@ -74,7 +74,7 @@ descriptions never enter agent instruction context.
74
74
  | @orkestrel/websocket | 0.0.7 |
75
75
  | @orkestrel/worker | 0.0.6 |
76
76
  | @orkestrel/workflow | 0.0.10 |
77
- | @orkestrel/workspace | 0.0.2 |
77
+ | @orkestrel/workspace | 0.0.3 |
78
78
 
79
79
  <!-- catalog:end -->
80
80
 
@@ -1261,7 +1261,15 @@ application-browser compiler, without adding another test project.
1261
1261
  `coreViteConfig`, `srcViteConfig`, `binViteConfig`, and `appViteConfig` emit the thin per-target
1262
1262
  wrappers. `coreViteConfig()` is parameterless and never imports or attaches browser CSS machinery;
1263
1263
  the root `srcCore` factory and its wrapper stay host-independent even when the workspace also owns a
1264
- browser target. `binTsconfig` emits the executable declaration scope; `rootViteConfig`,
1264
+ browser target. `srcViteConfig` takes the environment plus the blueprint's `name` and `src`, because
1265
+ its face declaration has to reach `src/core` through a specifier the published tarball carries.
1266
+ `bundleTypes` rolls the face up through API Extractor, which leaves each core re-export behind a
1267
+ relative `../core/index.ts` path no consumer can resolve; a workspace declaring `core` therefore
1268
+ emits a `beforeWriteFile` rewrite turning that path into `@orkestrel/<name>`, its own published root
1269
+ export. The rewrite matches the final face roll-up only — applying it to the intermediate
1270
+ declarations makes API Extractor analyse `src/core`'s source and abort — and a workspace with no
1271
+ `core` emits no rewrite at all. `binTsconfig` emits the executable declaration scope;
1272
+ `rootViteConfig`,
1265
1273
  `singleSrcViteConfig`, and `applicationViteConfig` emit the root configuration for a library-only,
1266
1274
  single non-core `src` environment, and application-bearing workspace respectively; and
1267
1275
  `policyViteProject`, `configViteProject`, `guidesViteProject`, `integrationViteProject`, and
@@ -1970,6 +1978,34 @@ it is not a general-purpose source analyzer. Generated workspaces receive the sa
1970
1978
  module as a host-origin file and run it as a dedicated Node-only `policy` test project over
1971
1979
  `tests/policy.test.ts`.
1972
1980
 
1981
+ **Fleet policy purity.** Both policy files are fleet-owned: scaffold copies its own
1982
+ [`tests/setupPolicy.ts`](../../tests/setupPolicy.ts) into every workspace verbatim and regenerates
1983
+ [`tests/policy.test.ts`](../../tests/policy.test.ts) from the shipped template, so a local addition
1984
+ to either is discarded at the next bump. A second pass therefore guards them against accumulating
1985
+ any one package's architecture. It derives the forbidden identifier tokens from the consuming
1986
+ workspace's own declared package name — the short name's upper-snake and Pascal spellings, deduped —
1987
+ so the pass states no package literal and can never report itself. It reports every identifier that
1988
+ begins with one of those tokens, and it rejects any string or template literal naming a
1989
+ source-environment path under the `src/` prefix. Policy names an environment without that prefix
1990
+ when it must name one at all, so the rule does not fight the files' real needs. The generated test
1991
+ sweeps both files and plants a violation built from the same derived token, so a clean sweep is
1992
+ evidence rather than an instrument that has never fired.
1993
+
1994
+ **Why the token match is a prefix.** A package's architecture reaches these files as identifiers
1995
+ named for the package — `MCP_PATH`, `RouterPlan` — so the identifier must _begin_ with the token.
1996
+ A word that merely holds the token somewhere inside it is fleet vocabulary, not one package's
1997
+ architecture, and the pass leaves it alone. That is the rule on its own terms, and it also settles
1998
+ the collision the fleet actually has: `@orkestrel/contract` derives `Contract`, which
1999
+ [`tests/setupPolicy.ts`](../../tests/setupPolicy.ts) holds inside `isContractProperty`, and that
2000
+ workspace now adopts the pass unchanged. The match stays case-sensitive, so `mcpValue` is not the
2001
+ `MCP` token.
2002
+
2003
+ One collision stays open, and its report is correct rather than a false positive. `@orkestrel/policy`
2004
+ derives `POLICY`, which both files use as an identifier prefix many times over — the pass would be
2005
+ reporting the vocabulary it is built from. A workspace cannot be named for that and adopt this pass;
2006
+ it renames or omits the pass. Every other name in the line derives tokens no policy identifier
2007
+ begins with.
2008
+
1973
2009
  **The configuration suite.** Policy reads source, the `config` project exercises the root
1974
2010
  configuration, and integration builds for real. Every generated workspace therefore receives a
1975
2011
  universal Node-only
@@ -2528,7 +2564,7 @@ viteMachinery([], ['core', 'browser']) // { browser: true, vue: true, output: tr
2528
2564
  renderViteTest([{ project: 'srcCore' }], false).includes('projects: [srcCore]') // true
2529
2565
  viteHeader(viteMachinery([], ['core', 'browser'])) // the shared header, with browser and Vue support
2530
2566
  coreViteConfig()
2531
- srcViteConfig('browser')
2567
+ srcViteConfig('browser', { name: 'router', src: ['core', 'browser'] })
2532
2568
  appViteConfig('server')
2533
2569
  policyViteProject()
2534
2570
  configViteProject()
@@ -112,6 +112,32 @@ export const CODING_SOURCE_EXTENSIONS: readonly string[] = Object.freeze([
112
112
  /** Production-source glob derived from the complete inspected extension vocabulary. */
113
113
  export const CODING_SOURCE_GLOB = `{app,src}/**/*.{${CODING_SOURCE_EXTENSIONS.join(',')}}`
114
114
 
115
+ /** Fleet-owned policy files that must stay free of any one package's architecture. */
116
+ export const POLICY_INFRASTRUCTURE_FILES: readonly string[] = Object.freeze([
117
+ 'tests/policy.test.ts',
118
+ 'tests/setupPolicy.ts',
119
+ ])
120
+
121
+ /** Source environments whose prefixed paths fleet policy must not name in a literal. */
122
+ export const POLICY_SOURCE_ENVIRONMENTS: readonly string[] = Object.freeze([
123
+ 'browser',
124
+ 'core',
125
+ 'server',
126
+ ])
127
+
128
+ /**
129
+ * The prefixed source-environment path fleet policy must not name in a literal.
130
+ *
131
+ * @remarks
132
+ * Built rather than written, so this file states no matching literal of its own
133
+ * and can never report itself. Policy names an environment without the source
134
+ * prefix when it must name one at all, which is why the prefix is the rule.
135
+ */
136
+ export const POLICY_ENVIRONMENT_PATTERN: RegExp = new RegExp(
137
+ `src/(?:${POLICY_SOURCE_ENVIRONMENTS.join('|')})/`,
138
+ 'u',
139
+ )
140
+
115
141
  /** Virtual source text used while binding one policy-inspected module. */
116
142
  export const POLICY_SOURCE_TEXTS: Map<string, string> = new Map()
117
143
 
@@ -767,3 +793,104 @@ export function inspectCodingWorkspace(
767
793
  }
768
794
  return violations
769
795
  }
796
+
797
+ /**
798
+ * Read one workspace's declared package name.
799
+ *
800
+ * @param root - The workspace root holding the package manifest
801
+ * @returns The declared package name
802
+ */
803
+ export function readPackageName(root: string): string {
804
+ const manifest: unknown = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'))
805
+ const name =
806
+ typeof manifest === 'object' && manifest !== null && 'name' in manifest
807
+ ? manifest.name
808
+ : undefined
809
+ if (typeof name !== 'string') throw new Error(`Package manifest at ${root} declares no name`)
810
+ return name
811
+ }
812
+
813
+ /**
814
+ * Derive the identifier prefixes fleet policy must not use from a package name.
815
+ *
816
+ * @param name - The declared package name, scoped or bare
817
+ * @returns The short name's upper-snake and Pascal spellings, deduped
818
+ *
819
+ * @example
820
+ * ```ts
821
+ * derivePolicyTokens('@orkestrel/my-router') // ['MY_ROUTER', 'MyRouter']
822
+ * ```
823
+ */
824
+ export function derivePolicyTokens(name: string): readonly string[] {
825
+ const short = name.slice(name.lastIndexOf('/') + 1)
826
+ const words = short.split(/[^A-Za-z0-9]+/u).filter((word) => word.length > 0)
827
+ const upper = words.map((word) => word.toUpperCase()).join('_')
828
+ const pascal = words.map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join('')
829
+ return [...new Set([upper, pascal])].filter((token) => token.length > 0)
830
+ }
831
+
832
+ /**
833
+ * Add package-architecture violations while traversing one fleet policy source tree.
834
+ *
835
+ * @remarks
836
+ * An identifier carries a package's architecture when it is named for that package,
837
+ * so it must begin with the token. A word that merely holds the token somewhere
838
+ * inside it belongs to the fleet's own vocabulary and is left alone.
839
+ */
840
+ export function inspectPolicyNode(
841
+ path: string,
842
+ node: ts.Node,
843
+ violations: string[],
844
+ tokens: readonly string[],
845
+ ): void {
846
+ if (ts.isIdentifier(node)) {
847
+ const token = tokens.find((candidate) => node.text.startsWith(candidate))
848
+ if (token !== undefined) {
849
+ violations.push(`${path}:${formatPolicyPosition(node)} forbids the ${token} package token`)
850
+ }
851
+ }
852
+ if (
853
+ (ts.isStringLiteral(node) || ts.isTemplateLiteralToken(node)) &&
854
+ POLICY_ENVIRONMENT_PATTERN.test(node.text)
855
+ ) {
856
+ violations.push(
857
+ `${path}:${formatPolicyPosition(node)} forbids a source-environment path literal`,
858
+ )
859
+ }
860
+ ts.forEachChild(node, (child) => inspectPolicyNode(path, child, violations, tokens))
861
+ }
862
+
863
+ /**
864
+ * Inspect one fleet policy module for a single package's architecture.
865
+ *
866
+ * @param path - The workspace-relative source path used in diagnostics
867
+ * @param content - The TypeScript source text to inspect
868
+ * @param tokens - The package identifier tokens no identifier may begin with
869
+ * @returns Every violation, in source-position order
870
+ */
871
+ export function inspectPolicyPurity(
872
+ path: string,
873
+ content: string,
874
+ tokens: readonly string[],
875
+ ): readonly string[] {
876
+ const violations: string[] = []
877
+ const source = ts.createSourceFile(path, content, ts.ScriptTarget.Latest, true)
878
+ inspectPolicyNode(path, source, violations, tokens)
879
+ return violations
880
+ }
881
+
882
+ /**
883
+ * Inspect every fleet policy module under one workspace.
884
+ *
885
+ * @param root - The workspace root whose manifest names the consuming package
886
+ * @returns Every violation across the fleet policy files, in file and position order
887
+ */
888
+ export function inspectPolicyWorkspace(root: string): readonly string[] {
889
+ const tokens = derivePolicyTokens(readPackageName(root))
890
+ const violations: string[] = []
891
+ for (const path of POLICY_INFRASTRUCTURE_FILES) {
892
+ const content = readFileSync(join(root, path), 'utf8')
893
+ violations.push(...inspectPolicyPurity(path, content, tokens))
894
+ }
895
+ return violations
896
+ }
@@ -275,7 +275,7 @@ var DEFAULT_VERSION = "0.0.1";
275
275
  /** The `engines.node` range the `blueprint` builder fills. */
276
276
  var DEFAULT_ENGINES = `>=${MINIMUM_NODE_VERSION}`;
277
277
  /** The devDependency range generated packages pin `@orkestrel/scaffold` at. */
278
- var SCAFFOLD_RANGE = "^0.0.22";
278
+ var SCAFFOLD_RANGE = "^0.0.23";
279
279
  /** Tooling versions shared by scaffold and every generated workspace. */
280
280
  var BASE_DEV_DEPENDENCIES = Object.freeze({
281
281
  "@microsoft/api-extractor": "^7.58.12",
@@ -4700,8 +4700,19 @@ exit 1
4700
4700
  content: `import { globSync } from 'node:fs'
4701
4701
  import { describe, expect, it } from 'vitest'
4702
4702
  import { isBrowserVuePath } from './setup.js'
4703
- import { inspectCodingLaw, inspectCodingWorkspace, isFunctionDomainPath } from './setupPolicy.js'{{vuePolicyImport}}
4704
-
4703
+ import {
4704
+ derivePolicyTokens,
4705
+ inspectCodingLaw,
4706
+ inspectCodingWorkspace,
4707
+ inspectPolicyPurity,
4708
+ inspectPolicyWorkspace,
4709
+ isFunctionDomainPath,
4710
+ POLICY_SOURCE_ENVIRONMENTS,
4711
+ readPackageName,
4712
+ } from './setupPolicy.js'{{vuePolicyImport}}
4713
+
4714
+ ${CONST_KEYWORD} POLICY_PLANTED_PATH = 'tests/setupPolicy.ts'
4715
+ ${CONST_KEYWORD} POLICY_TOKENS = derivePolicyTokens(readPackageName(process.cwd()))
4705
4716
  ${CONST_KEYWORD} FUNCTION_MODULE_PATH = 'app/browser/composables/useTheme.ts'
4706
4717
  ${CONST_KEYWORD} FUNCTION_MODULE_VIOLATION =
4707
4718
  FUNCTION_MODULE_PATH + ' declarations do not form one matching exported function implementation'
@@ -4840,6 +4851,30 @@ describe('repository coding law', () => {
4840
4851
  expect(inspectCodingLaw(path, content)).toEqual([])
4841
4852
  })
4842
4853
  })
4854
+
4855
+ describe('fleet policy purity', () => {
4856
+ it('keeps fleet policy files free of this package architecture', () => {
4857
+ expect(inspectPolicyWorkspace(process.cwd())).toEqual([])
4858
+ })
4859
+
4860
+ it('reports planted package architecture, so a clean sweep is evidence', () => {
4861
+ const token = POLICY_TOKENS[0]
4862
+ const environment = POLICY_SOURCE_ENVIRONMENTS[0]
4863
+ if (token === undefined || environment === undefined) {
4864
+ throw new Error('The package manifest derived no policy token to plant')
4865
+ }
4866
+ const planted = [
4867
+ 'export const ' + token + '_PATH = []',
4868
+ "export const path = 'src/" + environment + "/index.ts'",
4869
+ '',
4870
+ ].join('\\n')
4871
+
4872
+ expect(inspectPolicyPurity(POLICY_PLANTED_PATH, planted, POLICY_TOKENS)).toEqual([
4873
+ POLICY_PLANTED_PATH + ':1:14 forbids the ' + token + ' package token',
4874
+ POLICY_PLANTED_PATH + ':2:21 forbids a source-environment path literal',
4875
+ ])
4876
+ })
4877
+ })
4843
4878
  `
4844
4879
  }),
4845
4880
  setupServer: Object.freeze({
@@ -9288,28 +9323,60 @@ function srcTsconfig(environment) {
9288
9323
  * `build.lib` / externals live in the root `srcBrowser` / `srcServer` export
9289
9324
  * instead (per the live exemplars).
9290
9325
  *
9326
+ * @remarks
9327
+ * `bundleTypes` rolls the face up through API Extractor, which leaves every
9328
+ * `src/core` re-export behind a relative `../core/index.ts` specifier — a path no
9329
+ * published tarball carries. A workspace that also declares `core` therefore emits
9330
+ * a `beforeWriteFile` rewrite turning that specifier into `@orkestrel/<name>`, the
9331
+ * package's own published root export. The rewrite is narrowed to the FINAL face
9332
+ * roll-up: applying it to the intermediate declarations makes API Extractor analyse
9333
+ * `src/core`'s own source and abort. A workspace with no `core` emits no rewrite,
9334
+ * because it has no cross-environment specifier to externalize.
9335
+ *
9291
9336
  * @param environment - The non-`core` environment to derive the `vite.config.ts` for.
9337
+ * @param spec - The blueprint slice naming the package and its declared source environments.
9292
9338
  * @returns The environment `vite.config.ts` file content, newline-terminated.
9293
9339
  *
9294
9340
  * @example
9295
9341
  * ```ts
9296
- * srcViteConfig('browser').includes('srcBrowser') // true
9342
+ * srcViteConfig('browser', { name: 'router', src: ['core', 'browser'] }).includes('srcBrowser')
9297
9343
  * ```
9298
9344
  */
9299
- function srcViteConfig(environment) {
9345
+ function srcViteConfig(environment, spec) {
9300
9346
  const anchor = environment === "browser" ? "srcBrowser" : "srcServer";
9347
+ const specifier = `'@orkestrel/${spec.name}'`;
9348
+ const pattern = "/(?:\\.\\.\\/)+core\\/index\\.ts/g";
9349
+ const inlineCall = `\t\t\t\t\t\t? content.replaceAll(${pattern}, ${specifier})`;
9350
+ const call = fitsPrintWidth(inlineCall) ? [inlineCall] : [
9351
+ " ? content.replaceAll(",
9352
+ `\t\t\t\t\t\t\t\t${pattern},`,
9353
+ `\t\t\t\t\t\t\t\t${specifier},`,
9354
+ " )"
9355
+ ];
9356
+ const face = `/[\\\\/]dist[\\\\/]src[\\\\/]${environment}[\\\\/]index\\.d\\.ts$/`;
9357
+ const external = spec.src.includes("core");
9358
+ const rewrite = external ? `\n${[
9359
+ " beforeWriteFile: (path, content) => ({",
9360
+ `\t\t\t\t\tcontent: ${face}.test(path)`,
9361
+ ...call,
9362
+ " : content,",
9363
+ " }),"
9364
+ ].join("\n")}` : "";
9301
9365
  return `import { defineConfig } from 'vite'
9302
9366
  import dts from 'vite-plugin-dts'
9303
9367
  import { ${anchor}, resolveWorkspacePath } from '../../vite.config.ts'
9304
9368
 
9305
- // Types are bundled inline by vite-plugin-dts (see configs/src/vite.core.config.ts
9306
- // for the same pattern).
9369
+ ${external ? `// vite-plugin-dts rolls this face into one declaration, and the roll-up reaches
9370
+ // src/core through a relative source path the tarball does not carry. The rewrite
9371
+ // below externalizes core through the package's own published root export, on the
9372
+ // final roll-up only.` : `// vite-plugin-dts rolls this face into one declaration. This workspace declares no
9373
+ // src/core, so the roll-up has no cross-environment specifier to externalize.`}
9307
9374
  export default defineConfig(
9308
9375
  ${anchor}({
9309
9376
  plugins: [
9310
9377
  dts({
9311
9378
  tsconfigPath: resolveWorkspacePath('configs/src/tsconfig.${environment}.json'),
9312
- bundleTypes: true,
9379
+ bundleTypes: true,${rewrite}
9313
9380
  }),
9314
9381
  ],
9315
9382
  }),
@@ -9525,7 +9592,7 @@ function configArtifacts(spec) {
9525
9592
  const row = SRC_MATRIX[environment];
9526
9593
  for (const path of row.configs) {
9527
9594
  const isTsconfig = path.endsWith(".json");
9528
- const content = environment === "core" ? isTsconfig ? coreTsconfig() : coreViteConfig() : isTsconfig ? srcTsconfig(environment) : srcViteConfig(environment);
9595
+ const content = environment === "core" ? isTsconfig ? coreTsconfig() : coreViteConfig() : isTsconfig ? srcTsconfig(environment) : srcViteConfig(environment, spec);
9529
9596
  artifacts.push({
9530
9597
  path,
9531
9598
  group: "configs",