@orkestrel/scaffold 0.0.54 → 0.0.55

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.
@@ -128,6 +128,7 @@ Every file in the following table is a normative extension of this root. Read ev
128
128
  | `.claude/rules/application.md` | App composition, entries, manifest safety, lifecycle, integration |
129
129
  | `.claude/rules/browser.md` | Vue/browser architecture and platform usage |
130
130
  | `.claude/rules/styles.md` | SCSS/CSS centralization, tokens, mixins, layers, naming |
131
+ | `.claude/rules/portability.md` | Host branching, paths, line endings, processes, terminals, OS claims |
131
132
  | `.claude/rules/documentation.md` | Guides, parity, roadmap, showcase, examples |
132
133
  | `.claude/rules/writing.md` | Developer prose: reports, replies, vocabulary, claims, structure |
133
134
  | `.claude/rules/quality.md` | Research, dependency reuse, hardening, completion, package inspection |
@@ -44,9 +44,9 @@ so network-controlled descriptions never enter agent instruction context.
44
44
  | `@orkestrel/abort` | `0.0.8` | L1 | `@orkestrel/contract` `^0.0.13` |
45
45
  | `@orkestrel/agent` | `0.0.18` | L5 | `@orkestrel/abort` `^0.0.7`, `@orkestrel/budget` `^0.0.7`, `@orkestrel/contract` `^0.0.13`, `@orkestrel/database` `^0.0.11`, `@orkestrel/emitter` `^0.0.7`, `@orkestrel/queue` `^0.0.10`, `@orkestrel/timeout` `^0.0.7`, `@orkestrel/tool` `^0.0.11`, `@orkestrel/workflow` `^0.0.14`, `@orkestrel/workspace` `^0.0.5` |
46
46
  | `@orkestrel/brief` | `0.0.6` | L4 | `@orkestrel/contract` `^0.0.13`, `@orkestrel/emitter` `^0.0.8`, `@orkestrel/interpret` `^0.0.11`, `@orkestrel/reason` `^0.0.8` |
47
- | `@orkestrel/browser` | `0.0.13` | L3 | `@orkestrel/contract` `^0.0.13`, `@orkestrel/emitter` `^0.0.8`, `@orkestrel/html` `^0.0.6`, `@orkestrel/websocket` `^0.0.10` |
47
+ | `@orkestrel/browser` | `0.0.14` | L3 | `@orkestrel/contract` `^0.0.13`, `@orkestrel/emitter` `^0.0.8`, `@orkestrel/html` `^0.0.7`, `@orkestrel/websocket` `^0.0.10` |
48
48
  | `@orkestrel/budget` | `0.0.8` | L1 | `@orkestrel/contract` `^0.0.13` |
49
- | `@orkestrel/console` | `0.0.10` | L2 | `@orkestrel/contract` `^0.0.13`, `@orkestrel/emitter` `^0.0.8` |
49
+ | `@orkestrel/console` | `0.0.11` | L2 | `@orkestrel/contract` `^0.0.13`, `@orkestrel/emitter` `^0.0.8` |
50
50
  | `@orkestrel/contract` | `0.0.13` | L0 | |
51
51
  | `@orkestrel/csv` | `0.0.5` | L1 | `@orkestrel/contract` `^0.0.13` |
52
52
  | `@orkestrel/database` | `0.0.12` | L2 | `@orkestrel/contract` `^0.0.13`, `@orkestrel/emitter` `^0.0.8`, `@orkestrel/indexeddb` `^0.0.9`, `@orkestrel/sqlite` `^0.0.9` |
@@ -56,7 +56,7 @@ so network-controlled descriptions never enter agent instruction context.
56
56
  | `@orkestrel/html` | `0.0.7` | L1 | `@orkestrel/contract` `^0.0.13` |
57
57
  | `@orkestrel/indexeddb` | `0.0.9` | L1 | `@orkestrel/contract` `^0.0.13` |
58
58
  | `@orkestrel/interpret` | `0.0.11` | L3 | `@orkestrel/reason` `^0.0.8`, `@orkestrel/emitter` `^0.0.8`, `@orkestrel/contract` `^0.0.13`, `@orkestrel/template` `^0.0.5` |
59
- | `@orkestrel/lsp` | `0.0.2` | L3 | `@orkestrel/contract` `^0.0.13`, `@orkestrel/emitter` `^0.0.8`, `@orkestrel/process` `^0.0.6` |
59
+ | `@orkestrel/lsp` | `0.0.3` | L3 | `@orkestrel/contract` `^0.0.13`, `@orkestrel/emitter` `^0.0.8`, `@orkestrel/process` `^0.0.6` |
60
60
  | `@orkestrel/markdown` | `0.0.12` | L2 | `@orkestrel/contract` `^0.0.13`, `@orkestrel/html` `^0.0.7` |
61
61
  | `@orkestrel/mcp` | `0.0.25` | L3 | `@orkestrel/contract` `^0.0.13`, `@orkestrel/emitter` `^0.0.8`, `@orkestrel/process` `^0.0.6`, `@orkestrel/sse` `^0.0.5`, `@orkestrel/tool` `^0.0.12`, `@orkestrel/websocket` `^0.0.10` |
62
62
  | `@orkestrel/middleware` | `0.0.17` | L2 | `@orkestrel/abort` `^0.0.7`, `@orkestrel/budget` `^0.0.7`, `@orkestrel/contract` `^0.0.13`, `@orkestrel/timeout` `^0.0.7` |
@@ -64,7 +64,7 @@ so network-controlled descriptions never enter agent instruction context.
64
64
  | `@orkestrel/ndjson` | `0.0.8` | L1 | `@orkestrel/contract` `^0.0.13` |
65
65
  | `@orkestrel/ollama` | `0.0.12` | L6 | `@orkestrel/agent` `^0.0.18`, `@orkestrel/budget` `^0.0.8`, `@orkestrel/contract` `^0.0.13`, `@orkestrel/ndjson` `^0.0.8`, `@orkestrel/timeout` `^0.0.8`, `@orkestrel/tool` `^0.0.12` |
66
66
  | `@orkestrel/pool` | `0.0.8` | L2 | `@orkestrel/emitter` `^0.0.7` |
67
- | `@orkestrel/probe` | `0.0.8` | L4 | `@orkestrel/contract` `^0.0.13`, `@orkestrel/emitter` `^0.0.8`, `@orkestrel/lsp` `^0.0.2`, `@orkestrel/mcp` `^0.0.25`, `@orkestrel/queue` `^0.0.10`, `@orkestrel/timeout` `^0.0.8`, `@orkestrel/tool` `^0.0.12` |
67
+ | `@orkestrel/probe` | `0.0.9` | L4 | `@orkestrel/contract` `^0.0.13`, `@orkestrel/emitter` `^0.0.8`, `@orkestrel/lsp` `^0.0.3`, `@orkestrel/mcp` `^0.0.25`, `@orkestrel/queue` `^0.0.10`, `@orkestrel/timeout` `^0.0.8`, `@orkestrel/tool` `^0.0.12` |
68
68
  | `@orkestrel/process` | `0.0.6` | L2 | `@orkestrel/contract` `^0.0.13`, `@orkestrel/emitter` `^0.0.7` |
69
69
  | `@orkestrel/program` | `0.0.10` | L4 | `@orkestrel/contract` `^0.0.13`, `@orkestrel/emitter` `^0.0.7`, `@orkestrel/qualifier` `^0.0.11`, `@orkestrel/rater` `^0.0.11`, `@orkestrel/reason` `^0.0.7` |
70
70
  | `@orkestrel/qualifier` | `0.0.11` | L3 | `@orkestrel/contract` `^0.0.13`, `@orkestrel/emitter` `^0.0.7`, `@orkestrel/reason` `^0.0.7` |
@@ -73,7 +73,7 @@ so network-controlled descriptions never enter agent instruction context.
73
73
  | `@orkestrel/reason` | `0.0.8` | L2 | `@orkestrel/contract` `^0.0.13`, `@orkestrel/emitter` `^0.0.8` |
74
74
  | `@orkestrel/relation` | `0.0.10` | L3 | `@orkestrel/contract` `^0.0.13`, `@orkestrel/database` `^0.0.12`, `@orkestrel/emitter` `^0.0.8` |
75
75
  | `@orkestrel/router` | `0.0.11` | L2 | `@orkestrel/abort` `^0.0.7`, `@orkestrel/contract` `^0.0.13`, `@orkestrel/emitter` `^0.0.7` |
76
- | `@orkestrel/scaffold` | `0.0.53` | L3 | `@orkestrel/console` `^0.0.10`, `@orkestrel/contract` `^0.0.13`, `@orkestrel/emitter` `^0.0.8`, `@orkestrel/markdown` `^0.0.11`, `@orkestrel/process` `^0.0.6`, `@orkestrel/template` `^0.0.5` |
76
+ | `@orkestrel/scaffold` | `0.0.54` | L3 | `@orkestrel/console` `^0.0.10`, `@orkestrel/contract` `^0.0.13`, `@orkestrel/emitter` `^0.0.8`, `@orkestrel/markdown` `^0.0.12`, `@orkestrel/process` `^0.0.6`, `@orkestrel/template` `^0.0.5` |
77
77
  | `@orkestrel/sea` | `0.0.11` | L3 | `@orkestrel/contract` `^0.0.13`, `@orkestrel/emitter` `^0.0.8`, `@orkestrel/process` `^0.0.6` |
78
78
  | `@orkestrel/server` | `0.0.15` | L3 | `@orkestrel/abort` `^0.0.8`, `@orkestrel/contract` `^0.0.13`, `@orkestrel/emitter` `^0.0.8`, `@orkestrel/router` `^0.0.11`, `@orkestrel/timeout` `^0.0.8` |
79
79
  | `@orkestrel/sqlite` | `0.0.9` | L1 | `@orkestrel/contract` `^0.0.13` |
@@ -81,7 +81,7 @@ so network-controlled descriptions never enter agent instruction context.
81
81
  | `@orkestrel/supervisor` | `0.0.1` | L5 | `@orkestrel/contract` `^0.0.11`, `@orkestrel/database` `^0.0.9`, `@orkestrel/emitter` `^0.0.6`, `@orkestrel/workflow` `^0.0.12` |
82
82
  | `@orkestrel/table` | `0.0.3` | L2 | `@orkestrel/contract` `^0.0.13`, `@orkestrel/emitter` `^0.0.8` |
83
83
  | `@orkestrel/template` | `0.0.5` | L2 | `@orkestrel/contract` `^0.0.13`, `@orkestrel/emitter` `^0.0.8` |
84
- | `@orkestrel/terminal` | `0.0.12` | L3 | `@orkestrel/console` `^0.0.10`, `@orkestrel/contract` `^0.0.13`, `@orkestrel/database` `^0.0.12`, `@orkestrel/emitter` `^0.0.8`, `@orkestrel/form` `^0.0.3`, `@orkestrel/sse` `^0.0.5` |
84
+ | `@orkestrel/terminal` | `0.0.13` | L3 | `@orkestrel/console` `^0.0.11`, `@orkestrel/contract` `^0.0.13`, `@orkestrel/database` `^0.0.12`, `@orkestrel/emitter` `^0.0.8`, `@orkestrel/form` `^0.0.3`, `@orkestrel/sse` `^0.0.5` |
85
85
  | `@orkestrel/test` | `0.0.11` | L0 | |
86
86
  | `@orkestrel/timeout` | `0.0.8` | L1 | `@orkestrel/contract` `^0.0.13` |
87
87
  | `@orkestrel/tool` | `0.0.12` | L1 | `@orkestrel/contract` `^0.0.13` |
@@ -106,6 +106,17 @@ kind. It reads declaration syntax and file name, never meaning.
106
106
  no bare collection literal.
107
107
  - It proves that no source, test, config, or script file carries an `eslint-disable` or
108
108
  `oxlint-disable` directive.
109
+ - It proves that every `.claude/rules/*.md` file has a rule-map row in `AGENTS.md` and that every
110
+ row resolves to a file.
111
+ - It proves the host portability rules that are path- or text-shaped over the populations
112
+ `POLICY_PORTABILITY_GLOB` and `POLICY_PORTABILITY_SOURCE_GLOB` name: no path segment carries a
113
+ Windows reserved device name, a character Windows refuses, a trailing dot or space, or a sibling
114
+ differing only by case; no `package.json` script names a `.sh` file; and no source in the parsed
115
+ population trims a payload before splitting it on `'\n'`, reads `os.EOL`, or imports `EOL` from
116
+ `node:os`.
117
+ - It cannot write a filename the host refuses. A Windows host rejects `<`, folds a case collision
118
+ into one file, and turns `:` into an alternate data stream, so those boundaries are proven from a
119
+ path population rather than from written files.
109
120
  - It does not prove a collection is frozen. It reads the declaration, never the value a call
110
121
  returns, so `Object.freeze([…])` and any other call initializer are one syntax to it. The freeze
111
122
  obligation in the earlier kind-purity rules binds regardless; only the bare literal is mechanical.
@@ -0,0 +1,98 @@
1
+ ---
2
+ paths:
3
+ - 'src/**/*'
4
+ - 'app/**/*'
5
+ - 'configs/**/*'
6
+ - 'tests/**/*'
7
+ - 'scripts/**/*'
8
+ - 'guides/**/*'
9
+ - 'package.json'
10
+ - '.gitattributes'
11
+ - '.github/workflows/*'
12
+ ---
13
+
14
+ # Host portability rules
15
+
16
+ Published source runs on Windows and Linux. Write one implementation that is correct on each host,
17
+ or gate the host-specific branch on the host that needs it.
18
+
19
+ Take the test half from `.claude/rules/tests.md`: it governs a host-varying property inside a test
20
+ and the form of a conditional skip.
21
+
22
+ ## Host branching
23
+
24
+ - Branch on a capability a runtime probe answers — a returned value, a thrown code, a present
25
+ member — wherever the difference is detectable.
26
+ - Branch on `process.platform` only where the behavior is platform-defined, and name that fact in
27
+ the symbol's `@remarks`.
28
+ - Put a platform-literal table — install root, executable suffix, toolchain name — inside a
29
+ `process.platform` branch, and read it on that platform alone.
30
+ - Never flatten a correctly-gated host branch into a single path, and never relax a fail-closed
31
+ check to make one host pass.
32
+
33
+ ## Line endings
34
+
35
+ - Split arrived text on `/\r\n|\n/`. Never split on `\n` alone, and never split on a bare `\r`.
36
+ - Split first, then trim each line; trimming the whole payload leaves a `\r` on every line but the
37
+ last.
38
+ - Frame a named protocol — HTTP, LSP, RFC 6455, NDJSON — with the terminator that protocol fixes,
39
+ and leave that terminator alone.
40
+ - Emit `\n` from code this package owns. Never emit `os.EOL`.
41
+
42
+ ## Paths
43
+
44
+ - Normalize each operand of a path comparison with the same normalizer before comparing.
45
+ - Never case-fold a containment check. Fold a lookup key only by the file-name case sensitivity the
46
+ consumer declares, and fold the whole key when you fold at all.
47
+ - Compose, split, and relativize a host path with `node:path`. Never concatenate a separator
48
+ literal.
49
+ - Keep a storage key, an archive key, and a URI path slash-separated, and refuse `\` and a drive
50
+ prefix inside one.
51
+ - Refuse the Windows reserved device names and the filename characters Windows rejects, on every
52
+ host.
53
+ - Read the temporary directory from `os.tmpdir()`. Never write a `/tmp` literal in source.
54
+ - Build a `file:` URI with `pathToFileURL`. Never format one from a path string.
55
+
56
+ ## Processes and executables
57
+
58
+ - Resolve, spawn, and terminate a child through `@orkestrel/process` where the package declares it.
59
+ - Where it is not declared, spawn `process.execPath` with a JavaScript entry. Never spawn a `.bin`
60
+ shim, and never add `shell: true` to reach one.
61
+ - Take a resolver's first match only after splitting its output into lines and trimming each one.
62
+ `where` prints a match per line and can name a file that is not an executable.
63
+ - Treat `fs.constants.X_OK` as an existence check on Windows, where `accessSync` passes on a plain
64
+ file. Confirm an executable by its name and its file type instead.
65
+ - Read an `fs.constants` member before putting it in a flag word. Windows leaves `O_NOFOLLOW`
66
+ undefined, so `O_WRONLY | O_NOFOLLOW` refuses nothing there. Never polyfill the missing member:
67
+ compensate with a check the host supports, or record the limit.
68
+ - Read `PROGRAMFILES`, `LOCALAPPDATA`, and `APPDATA` inside the win32 branch and `HOME` inside the
69
+ POSIX branch. Never read one host's root on the other.
70
+ - Terminate a Windows child by its process tree. A `SIGTERM` handler never runs there, so gate every
71
+ cooperative-shutdown path on POSIX.
72
+ - Treat a permission bit as advisory on Windows: a written mode does not round-trip
73
+ (`chmodSync(dir, 0o500)` reads back `0444` there), so assert modes only where a probe reports they
74
+ round-trip.
75
+ - Retry an `EBUSY`, `ENOTEMPTY`, or `EPERM` removal inside a bounded budget, and report the failure
76
+ when the budget ends.
77
+
78
+ ## Terminals
79
+
80
+ - Gate styling and interactivity on `isTTY`, `NO_COLOR`, and `FORCE_COLOR`, never on the platform.
81
+ - Decode `\r`, `\n`, and `\r\n` from a key stream as the same key.
82
+ - Never call a Windows console-mode API. Node exposes none, so record the unreached behavior as a
83
+ limit.
84
+
85
+ ## Scripts and packaging
86
+
87
+ - Write every `package.json` script as a portable command: a Node invocation or an installed binary.
88
+ Never name a `.sh` file there.
89
+ - Keep `#!/usr/bin/env node` on an npm bin. npm writes the Windows shim from it.
90
+ - Keep an agent hook under `scripts/` in bash, and keep `.gitattributes` `eol=lf`.
91
+
92
+ ## Claims
93
+
94
+ - Never describe the suite, the host, or CI as POSIX. Name the host and the date behind a reading.
95
+ - Run the gate chain on each host a change claims, and record a reading that could not run as a
96
+ limit naming the host it needs.
97
+ - Keep a POSIX-shaped `file:///` literal in a guide only as an opaque fictional identifier. Build
98
+ any example a reader executes with `pathToFileURL`.
@@ -4,7 +4,7 @@
4
4
  "storage": "AGENTS.md",
5
5
  "destination": "AGENTS.md",
6
6
  "executable": false,
7
- "digest": "5dbb143394991882d36f90a8aabd7e6b0874863b45dde4040760fc834e9d942e"
7
+ "digest": "43ce847aa96a525919dbe948a2311214c312d58069054800577e5aff89024f08"
8
8
  },
9
9
  {
10
10
  "storage": "CLAUDE.md",
@@ -310,7 +310,7 @@
310
310
  "storage": "claude/agents/orkestrel.md",
311
311
  "destination": ".claude/agents/orkestrel.md",
312
312
  "executable": false,
313
- "digest": "f71a28e5a4865bf6a910d9628d645f7d430f38aef29b815ef85caa1c01d87454"
313
+ "digest": "eacfb94cd758ed202aba25ac759420223bae1e624a7baf208feaeb9c979e3c9f"
314
314
  },
315
315
  {
316
316
  "storage": "claude/agents/planner.md",
@@ -358,7 +358,7 @@
358
358
  "storage": "claude/rules/architecture.md",
359
359
  "destination": ".claude/rules/architecture.md",
360
360
  "executable": false,
361
- "digest": "94f70c20f927129cf9cb3965b3180a14e941e4f6eddf6512c4adc014e0bcbdf2"
361
+ "digest": "674bcc17f8127948e11391cbbedfd7396911d88c0e99cfefe718f718ed06a333"
362
362
  },
363
363
  {
364
364
  "storage": "claude/rules/browser.md",
@@ -384,6 +384,12 @@
384
384
  "executable": false,
385
385
  "digest": "c128dace792fcc042446f3d35dce90ef3918d44449a424dbd6dcea4dfcab24f5"
386
386
  },
387
+ {
388
+ "storage": "claude/rules/portability.md",
389
+ "destination": ".claude/rules/portability.md",
390
+ "executable": false,
391
+ "digest": "b948c92e3409efd01b45ed9e8d917f4713cda0ecd5ea343932613b2977a614ff"
392
+ },
387
393
  {
388
394
  "storage": "claude/rules/quality.md",
389
395
  "destination": ".claude/rules/quality.md",
@@ -682,13 +688,13 @@
682
688
  "storage": "tests/policy.test.ts",
683
689
  "destination": "tests/policy.test.ts",
684
690
  "executable": false,
685
- "digest": "4642dfcfe2c2d928373ba42026323c4eba976908dd4382ee2ff1d1faf5d766a2"
691
+ "digest": "6a2be29c887e90b976273f8ef9da9ac1c17ed8c1880c133eb8a67514ddf5b5c3"
686
692
  },
687
693
  {
688
694
  "storage": "tests/setupPolicy.ts",
689
695
  "destination": "tests/setupPolicy.ts",
690
696
  "executable": false,
691
- "digest": "3714a96bda5aeb5bbdcc8223d33a7ef77562214303d6c58acfec33af182bdc45"
697
+ "digest": "e33cc2f49a5bc201e339f50c59d280d36d06a5b687bc080a00b41f09577fa7c7"
692
698
  }
693
699
  ],
694
700
  "roots": [
@@ -736,5 +742,5 @@
736
742
  ".codex/agents",
737
743
  ".cursor/rules"
738
744
  ],
739
- "digest": "bb4f3428a2d0d631ca77df5dc1921a7a171cef0b5e8a840038f0b7ab32f751d7"
745
+ "digest": "900545f6b4a0078f1ab0256867453576ade0fca62344a3fd258d249cc6853823"
740
746
  }
@@ -5,7 +5,9 @@ import {
5
5
  FUNCTION_SOURCE_FILES,
6
6
  GENERIC_POLICY_SOURCES,
7
7
  inspectPolicyControl,
8
+ inspectPolicyFilenamePaths,
8
9
  inspectPolicyMirrorPaths,
10
+ inspectPolicyPortability,
9
11
  inspectPolicySources,
10
12
  inspectPolicyWorkspace,
11
13
  inspectSkillFamily,
@@ -14,7 +16,13 @@ import {
14
16
  parseSkillFrontmatter,
15
17
  POLICY_CONTROLS,
16
18
  POLICY_SUPPRESSION_DIRECTIVE,
19
+ PORTABILITY_POLICY_CONTROLS,
20
+ PORTABILITY_POLICY_EXCLUSION,
21
+ PORTABILITY_POLICY_LOCAL,
22
+ PORTABILITY_POLICY_SPLIT,
23
+ readPolicyPaths,
17
24
  readSkillFamily,
25
+ RULES_POLICY_CONTROLS,
18
26
  SKILL_POLICY_APOSTROPHE,
19
27
  SKILL_POLICY_BACKTICKED,
20
28
  SKILL_POLICY_CONTROLS,
@@ -413,8 +421,84 @@ describe('skill bridge policy', () => {
413
421
  }
414
422
  })
415
423
 
424
+ describe('rule map policy', () => {
425
+ for (const control of RULES_POLICY_CONTROLS) {
426
+ it(`${control.label} [membership: ${control.membership}]`, () => {
427
+ const violations = inspectPolicyControl(control)
428
+ expect(violations).toHaveLength(1)
429
+ expect(violations[0]?.rule).toBe(control.rule)
430
+ expect(control.message === undefined || violations[0]?.message === control.message).toBe(true)
431
+ })
432
+ }
433
+ })
434
+
435
+ describe('portability policy', () => {
436
+ for (const control of PORTABILITY_POLICY_CONTROLS) {
437
+ it(`${control.label} [membership: ${control.membership}]`, () => {
438
+ const violations = inspectPolicyControl(control)
439
+ expect(violations).toHaveLength(1)
440
+ expect(violations[0]?.rule).toBe(control.rule)
441
+ expect(control.message === undefined || violations[0]?.message === control.message).toBe(true)
442
+ })
443
+ }
444
+
445
+ for (const control of [
446
+ PORTABILITY_POLICY_EXCLUSION,
447
+ PORTABILITY_POLICY_LOCAL,
448
+ PORTABILITY_POLICY_SPLIT,
449
+ ]) {
450
+ it(`${control.label} [membership: ${control.membership}]`, () => {
451
+ expect(inspectPolicyControl(control)).toEqual([])
452
+ })
453
+ }
454
+
455
+ it('rejects a character Windows refuses inside a path segment', () => {
456
+ // A Windows host refuses to create this name, so the population itself is the control.
457
+ expect(inspectPolicyFilenamePaths(['src/worker/read<write>.ts'])).toEqual([
458
+ {
459
+ rule: 'portability',
460
+ path: 'src/worker/read<write>.ts',
461
+ message: 'path segments avoid the characters Windows refuses',
462
+ },
463
+ ])
464
+ })
465
+
466
+ it('rejects sibling paths that differ by case alone', () => {
467
+ // A Windows host folds the pair into one file, so the population itself is the control.
468
+ expect(inspectPolicyFilenamePaths(['guides/Readme.md', 'guides/readme.md'])).toEqual([
469
+ {
470
+ rule: 'portability',
471
+ path: 'guides/readme.md',
472
+ message: 'path differs from guides/Readme.md by case alone',
473
+ },
474
+ ])
475
+ })
476
+
477
+ it('accepts sibling paths that differ by more than case', () => {
478
+ expect(
479
+ inspectPolicyFilenamePaths([
480
+ 'guides/readme.md',
481
+ 'guides/readmes.md',
482
+ 'src/worker/helpers.ts',
483
+ ]),
484
+ ).toEqual([])
485
+ })
486
+ })
487
+
416
488
  describe('repository policy', () => {
417
489
  it('enforces placement and mirrors over the real workspace', () => {
418
490
  expect(inspectPolicyWorkspace(process.cwd())).toEqual([])
419
491
  })
492
+
493
+ it('reaches every branch of the workspace-authored path population', () => {
494
+ const paths = readPolicyPaths(process.cwd())
495
+ expect(paths).toContain('tests/setupPolicy.ts')
496
+ expect(paths).toContain('.claude/rules/names.md')
497
+ expect(paths).toContain('package.json')
498
+ expect(paths).toContain('.gitattributes')
499
+ })
500
+
501
+ it('keeps every workspace path, script, and source portable', () => {
502
+ expect(inspectPolicyPortability(process.cwd())).toEqual([])
503
+ })
420
504
  })