@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.
- package/dist/host/AGENTS.md +1 -0
- package/dist/host/claude/agents/orkestrel.md +6 -6
- package/dist/host/claude/rules/architecture.md +11 -0
- package/dist/host/claude/rules/portability.md +98 -0
- package/dist/host/manifest.json +12 -6
- package/dist/host/tests/policy.test.ts +84 -0
- package/dist/host/tests/setupPolicy.ts +540 -4
- package/dist/src/core/index.cjs +4 -4
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.js +4 -4
- package/dist/src/core/index.js.map +1 -1
- package/package.json +4 -4
|
@@ -23,6 +23,8 @@ export type PolicyRule =
|
|
|
23
23
|
| 'function'
|
|
24
24
|
| 'mirror'
|
|
25
25
|
| 'parser'
|
|
26
|
+
| 'portability'
|
|
27
|
+
| 'rules'
|
|
26
28
|
| 'skill'
|
|
27
29
|
| 'suppression'
|
|
28
30
|
| 'type'
|
|
@@ -266,6 +268,61 @@ export const POLICY_SUPPRESSION_PATTERN = new RegExp(
|
|
|
266
268
|
'u',
|
|
267
269
|
)
|
|
268
270
|
|
|
271
|
+
/** The workspace-authored path population inspected for a name a Windows checkout cannot hold. */
|
|
272
|
+
export const POLICY_PORTABILITY_GLOB: readonly string[] = Object.freeze([
|
|
273
|
+
'{src,app,configs,tests,scripts,guides}/**/*',
|
|
274
|
+
'{.agents,.claude,.codex,.cursor,.github}/**/*',
|
|
275
|
+
'*',
|
|
276
|
+
'.*',
|
|
277
|
+
])
|
|
278
|
+
|
|
279
|
+
/** The TypeScript source population parsed for host-specific line-ending handling. */
|
|
280
|
+
export const POLICY_PORTABILITY_SOURCE_GLOB = '{src,app,configs}/**/*.ts'
|
|
281
|
+
|
|
282
|
+
/** Every device name Windows reserves, whatever extension the segment carries. */
|
|
283
|
+
export const POLICY_RESERVED_NAMES: readonly string[] = Object.freeze([
|
|
284
|
+
'aux',
|
|
285
|
+
'com1',
|
|
286
|
+
'com2',
|
|
287
|
+
'com3',
|
|
288
|
+
'com4',
|
|
289
|
+
'com5',
|
|
290
|
+
'com6',
|
|
291
|
+
'com7',
|
|
292
|
+
'com8',
|
|
293
|
+
'com9',
|
|
294
|
+
'con',
|
|
295
|
+
'lpt1',
|
|
296
|
+
'lpt2',
|
|
297
|
+
'lpt3',
|
|
298
|
+
'lpt4',
|
|
299
|
+
'lpt5',
|
|
300
|
+
'lpt6',
|
|
301
|
+
'lpt7',
|
|
302
|
+
'lpt8',
|
|
303
|
+
'lpt9',
|
|
304
|
+
'nul',
|
|
305
|
+
'prn',
|
|
306
|
+
])
|
|
307
|
+
|
|
308
|
+
/** Every character Windows refuses inside a path segment. */
|
|
309
|
+
export const POLICY_RESERVED_PATTERN = /[<>:"|?*]/u
|
|
310
|
+
|
|
311
|
+
/** A shell script named as a complete path token inside a manifest script. */
|
|
312
|
+
export const POLICY_SHELL_PATTERN = /\.sh\b/u
|
|
313
|
+
|
|
314
|
+
/** The directory whose direct Markdown files form the complete rule family. */
|
|
315
|
+
export const POLICY_RULE_ROOT = '.claude/rules'
|
|
316
|
+
|
|
317
|
+
/** The root instruction file whose rule map registers the rule family. */
|
|
318
|
+
export const POLICY_RULE_MAP_FILE = 'AGENTS.md'
|
|
319
|
+
|
|
320
|
+
/** The heading that opens the root instruction file's rule map table. */
|
|
321
|
+
export const POLICY_RULE_MAP_HEADING = '## Rule map'
|
|
322
|
+
|
|
323
|
+
/** The workspace manifest whose scripts run on every supported host. */
|
|
324
|
+
export const POLICY_MANIFEST_FILE = 'package.json'
|
|
325
|
+
|
|
269
326
|
/**
|
|
270
327
|
* Normalize platform separators for stable matching and diagnostics.
|
|
271
328
|
*
|
|
@@ -736,10 +793,14 @@ export function inspectPolicySources(sources: readonly PolicySource[]): readonly
|
|
|
736
793
|
* Read the parsed TypeScript source population beneath one workspace.
|
|
737
794
|
*
|
|
738
795
|
* @param root - The workspace root to read.
|
|
739
|
-
* @
|
|
796
|
+
* @param glob - The population to read. Default: the src and app placement population.
|
|
797
|
+
* @returns Every non-ambient TypeScript source the population names, sorted by path.
|
|
740
798
|
*/
|
|
741
|
-
export function readPolicySources(
|
|
742
|
-
|
|
799
|
+
export function readPolicySources(
|
|
800
|
+
root: string,
|
|
801
|
+
glob: string | readonly string[] = POLICY_SOURCE_GLOB,
|
|
802
|
+
): readonly PolicySource[] {
|
|
803
|
+
return globSync(glob, { cwd: root })
|
|
743
804
|
.map(normalizePolicyPath)
|
|
744
805
|
.filter((path) => !POLICY_AMBIENT_SUFFIXES.some((suffix) => basename(path).endsWith(suffix)))
|
|
745
806
|
.sort()
|
|
@@ -1561,11 +1622,323 @@ export function inspectSkillBridges(root: string): readonly PolicyViolation[] {
|
|
|
1561
1622
|
return violations
|
|
1562
1623
|
}
|
|
1563
1624
|
|
|
1625
|
+
/**
|
|
1626
|
+
* Read every rule path the root instruction file's rule map registers.
|
|
1627
|
+
*
|
|
1628
|
+
* @param content - The raw root instruction text.
|
|
1629
|
+
* @returns Each backticked first cell beneath the rule map heading, in table order.
|
|
1630
|
+
*/
|
|
1631
|
+
export function readPolicyRuleMap(content: string): readonly string[] {
|
|
1632
|
+
const lines = content.replaceAll('\r\n', '\n').split('\n')
|
|
1633
|
+
const heading = lines.indexOf(POLICY_RULE_MAP_HEADING)
|
|
1634
|
+
if (heading === -1) return []
|
|
1635
|
+
const paths: string[] = []
|
|
1636
|
+
for (let index = heading + 1; index < lines.length; index += 1) {
|
|
1637
|
+
const line = lines[index]
|
|
1638
|
+
if (line === undefined || line.startsWith('## ')) break
|
|
1639
|
+
const cell = line.match(/^\|\s*`([^`]+)`\s*\|/u)?.[1]
|
|
1640
|
+
if (cell !== undefined) paths.push(normalizePolicyPath(cell))
|
|
1641
|
+
}
|
|
1642
|
+
return paths
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
/**
|
|
1646
|
+
* Inspect the discovered rule family against the root instruction file's rule map.
|
|
1647
|
+
*
|
|
1648
|
+
* A workspace with no rule file has no rule map to keep, so the population is empty there.
|
|
1649
|
+
*
|
|
1650
|
+
* @param root - The workspace root to inspect.
|
|
1651
|
+
* @returns Every unregistered rule file, then every row resolving to no file.
|
|
1652
|
+
*/
|
|
1653
|
+
export function inspectPolicyRuleMap(root: string): readonly PolicyViolation[] {
|
|
1654
|
+
const directory = resolvePolicyDirectory(root, POLICY_RULE_ROOT)
|
|
1655
|
+
if (directory === undefined) return []
|
|
1656
|
+
const rules = readdirSync(directory, { withFileTypes: true })
|
|
1657
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith('.md'))
|
|
1658
|
+
.map((entry) => `${POLICY_RULE_ROOT}/${entry.name}`)
|
|
1659
|
+
.sort()
|
|
1660
|
+
if (rules.length === 0) return []
|
|
1661
|
+
const content = isPolicyFile(root, POLICY_RULE_MAP_FILE)
|
|
1662
|
+
? readFileSync(join(root, POLICY_RULE_MAP_FILE), 'utf8')
|
|
1663
|
+
: ''
|
|
1664
|
+
const registered = new Set(readPolicyRuleMap(content))
|
|
1665
|
+
const violations: PolicyViolation[] = []
|
|
1666
|
+
for (const rule of rules) {
|
|
1667
|
+
if (!registered.has(rule)) {
|
|
1668
|
+
violations.push(createPolicyViolation('rules', rule, 'the rule map names every rule file'))
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
for (const path of registered) {
|
|
1672
|
+
if (!isPolicyFile(root, path)) {
|
|
1673
|
+
violations.push(
|
|
1674
|
+
createPolicyViolation('rules', path, 'every rule-map row resolves to a rule file'),
|
|
1675
|
+
)
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
return violations
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
/**
|
|
1682
|
+
* Inspect an explicit path population for a name a Windows checkout cannot hold.
|
|
1683
|
+
*
|
|
1684
|
+
* Each path is read through its own final segment, because the population lists every directory as
|
|
1685
|
+
* its own entry. A Windows host refuses the reserved characters and folds a case collision into one
|
|
1686
|
+
* file, so those two boundaries are proven from a path population rather than from written files.
|
|
1687
|
+
*
|
|
1688
|
+
* @param paths - The workspace-relative paths to inspect.
|
|
1689
|
+
* @returns Every unusable-name and case-collision violation in path order.
|
|
1690
|
+
*/
|
|
1691
|
+
export function inspectPolicyFilenamePaths(paths: readonly string[]): readonly PolicyViolation[] {
|
|
1692
|
+
const violations: PolicyViolation[] = []
|
|
1693
|
+
const folded = new Map<string, string>()
|
|
1694
|
+
for (const candidate of paths) {
|
|
1695
|
+
const path = normalizePolicyPath(candidate)
|
|
1696
|
+
const segment = basename(path)
|
|
1697
|
+
const [stem = ''] = segment.split('.')
|
|
1698
|
+
if (POLICY_RESERVED_NAMES.includes(stem.toLowerCase())) {
|
|
1699
|
+
violations.push(
|
|
1700
|
+
createPolicyViolation(
|
|
1701
|
+
'portability',
|
|
1702
|
+
path,
|
|
1703
|
+
'path segments avoid the names Windows reserves',
|
|
1704
|
+
),
|
|
1705
|
+
)
|
|
1706
|
+
}
|
|
1707
|
+
if (POLICY_RESERVED_PATTERN.test(segment)) {
|
|
1708
|
+
violations.push(
|
|
1709
|
+
createPolicyViolation(
|
|
1710
|
+
'portability',
|
|
1711
|
+
path,
|
|
1712
|
+
'path segments avoid the characters Windows refuses',
|
|
1713
|
+
),
|
|
1714
|
+
)
|
|
1715
|
+
}
|
|
1716
|
+
if (segment.endsWith('.') || segment.endsWith(' ')) {
|
|
1717
|
+
violations.push(
|
|
1718
|
+
createPolicyViolation(
|
|
1719
|
+
'portability',
|
|
1720
|
+
path,
|
|
1721
|
+
'path segments end with neither a dot nor a space',
|
|
1722
|
+
),
|
|
1723
|
+
)
|
|
1724
|
+
}
|
|
1725
|
+
const key = path.toLowerCase()
|
|
1726
|
+
const previous = folded.get(key)
|
|
1727
|
+
if (previous === undefined) {
|
|
1728
|
+
folded.set(key, path)
|
|
1729
|
+
} else if (previous !== path) {
|
|
1730
|
+
violations.push(
|
|
1731
|
+
createPolicyViolation('portability', path, `path differs from ${previous} by case alone`),
|
|
1732
|
+
)
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
return violations
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
/**
|
|
1739
|
+
* Read the workspace-authored path population, directories included.
|
|
1740
|
+
*
|
|
1741
|
+
* @param root - The workspace root to read.
|
|
1742
|
+
* @returns Every authored path, sorted by path.
|
|
1743
|
+
*/
|
|
1744
|
+
export function readPolicyPaths(root: string): readonly string[] {
|
|
1745
|
+
return globSync(POLICY_PORTABILITY_GLOB, { cwd: root }).map(normalizePolicyPath).sort()
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
/**
|
|
1749
|
+
* Inspect the workspace-authored path population for a name a Windows checkout cannot hold.
|
|
1750
|
+
*
|
|
1751
|
+
* @param root - The workspace root to inspect.
|
|
1752
|
+
* @returns Every unusable-name and case-collision violation in path order.
|
|
1753
|
+
*/
|
|
1754
|
+
export function inspectPolicyFilenames(root: string): readonly PolicyViolation[] {
|
|
1755
|
+
return inspectPolicyFilenamePaths(readPolicyPaths(root))
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
/**
|
|
1759
|
+
* Parse the manifest's script record without interpreting any other manifest field.
|
|
1760
|
+
*
|
|
1761
|
+
* @param content - The raw package.json text.
|
|
1762
|
+
* @returns Each script name and its command, in manifest order.
|
|
1763
|
+
*/
|
|
1764
|
+
export function parsePolicyScripts(content: string): ReadonlyMap<string, string> {
|
|
1765
|
+
const scripts = new Map<string, string>()
|
|
1766
|
+
let manifest: unknown
|
|
1767
|
+
try {
|
|
1768
|
+
manifest = JSON.parse(content)
|
|
1769
|
+
} catch {
|
|
1770
|
+
return scripts
|
|
1771
|
+
}
|
|
1772
|
+
if (typeof manifest !== 'object' || manifest === null || Array.isArray(manifest)) return scripts
|
|
1773
|
+
const record: unknown = Object.getOwnPropertyDescriptor(manifest, 'scripts')?.value
|
|
1774
|
+
if (typeof record !== 'object' || record === null || Array.isArray(record)) return scripts
|
|
1775
|
+
for (const name of Object.getOwnPropertyNames(record)) {
|
|
1776
|
+
const command: unknown = Object.getOwnPropertyDescriptor(record, name)?.value
|
|
1777
|
+
if (typeof command === 'string') scripts.set(name, command)
|
|
1778
|
+
}
|
|
1779
|
+
return scripts
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
/**
|
|
1783
|
+
* Inspect every manifest script for a shell file no Windows host runs.
|
|
1784
|
+
*
|
|
1785
|
+
* @param root - The workspace root to inspect.
|
|
1786
|
+
* @returns Every shell-script violation in manifest order.
|
|
1787
|
+
*/
|
|
1788
|
+
export function inspectPolicyScripts(root: string): readonly PolicyViolation[] {
|
|
1789
|
+
if (!isPolicyFile(root, POLICY_MANIFEST_FILE)) return []
|
|
1790
|
+
const content = readFileSync(join(root, POLICY_MANIFEST_FILE), 'utf8')
|
|
1791
|
+
const violations: PolicyViolation[] = []
|
|
1792
|
+
for (const [name, command] of parsePolicyScripts(content)) {
|
|
1793
|
+
if (POLICY_SHELL_PATTERN.test(command)) {
|
|
1794
|
+
violations.push(
|
|
1795
|
+
createPolicyViolation(
|
|
1796
|
+
'portability',
|
|
1797
|
+
POLICY_MANIFEST_FILE,
|
|
1798
|
+
`manifest scripts name no .sh file: ${name}`,
|
|
1799
|
+
),
|
|
1800
|
+
)
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
return violations
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
/**
|
|
1807
|
+
* Whether a call trims a whole payload before splitting it on a line feed.
|
|
1808
|
+
*
|
|
1809
|
+
* @param node - The syntax node to inspect.
|
|
1810
|
+
* @returns True when the node is the split chain that leaves a carriage return on each line but
|
|
1811
|
+
* the last; false otherwise.
|
|
1812
|
+
*/
|
|
1813
|
+
export function matchesPolicySplit(node: ts.Node): boolean {
|
|
1814
|
+
if (!ts.isCallExpression(node) || node.arguments.length !== 1) return false
|
|
1815
|
+
const split = node.expression
|
|
1816
|
+
if (!ts.isPropertyAccessExpression(split) || split.name.text !== 'split') return false
|
|
1817
|
+
const argument = node.arguments[0]
|
|
1818
|
+
if (argument === undefined) return false
|
|
1819
|
+
if (!ts.isStringLiteral(argument) && !ts.isNoSubstitutionTemplateLiteral(argument)) return false
|
|
1820
|
+
if (argument.text !== '\n') return false
|
|
1821
|
+
const trim = split.expression
|
|
1822
|
+
return (
|
|
1823
|
+
ts.isCallExpression(trim) &&
|
|
1824
|
+
trim.arguments.length === 0 &&
|
|
1825
|
+
ts.isPropertyAccessExpression(trim.expression) &&
|
|
1826
|
+
trim.expression.name.text === 'trim'
|
|
1827
|
+
)
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
/**
|
|
1831
|
+
* Whether an expression reads the host line ending from a binding named os.
|
|
1832
|
+
*
|
|
1833
|
+
* @param node - The syntax node to inspect.
|
|
1834
|
+
* @returns True for an EOL member read on a binding named os; false otherwise.
|
|
1835
|
+
*/
|
|
1836
|
+
export function matchesPolicyTerminator(node: ts.Node): boolean {
|
|
1837
|
+
return (
|
|
1838
|
+
ts.isPropertyAccessExpression(node) &&
|
|
1839
|
+
node.name.text === 'EOL' &&
|
|
1840
|
+
ts.isIdentifier(node.expression) &&
|
|
1841
|
+
node.expression.text === 'os'
|
|
1842
|
+
)
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
/**
|
|
1846
|
+
* Whether an import declaration takes the EOL member from the host module.
|
|
1847
|
+
*
|
|
1848
|
+
* @param node - The syntax node to inspect.
|
|
1849
|
+
* @returns True for a named EOL import from node:os or os; false otherwise.
|
|
1850
|
+
*/
|
|
1851
|
+
export function importsPolicyTerminator(node: ts.Node): boolean {
|
|
1852
|
+
if (!ts.isImportDeclaration(node)) return false
|
|
1853
|
+
const specifier = node.moduleSpecifier
|
|
1854
|
+
if (!ts.isStringLiteral(specifier)) return false
|
|
1855
|
+
if (specifier.text !== 'node:os' && specifier.text !== 'os') return false
|
|
1856
|
+
const bindings = node.importClause?.namedBindings
|
|
1857
|
+
if (bindings === undefined || !ts.isNamedImports(bindings)) return false
|
|
1858
|
+
return bindings.elements.some((element) => (element.propertyName ?? element.name).text === 'EOL')
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
/**
|
|
1862
|
+
* Inspect one syntax node and its descendants for host-specific line-ending handling.
|
|
1863
|
+
*
|
|
1864
|
+
* @param path - The workspace-relative source path.
|
|
1865
|
+
* @param node - The syntax node to inspect.
|
|
1866
|
+
* @returns Every line-ending violation in source order.
|
|
1867
|
+
*/
|
|
1868
|
+
export function inspectPolicyEndingNode(path: string, node: ts.Node): readonly PolicyViolation[] {
|
|
1869
|
+
const violations: PolicyViolation[] = []
|
|
1870
|
+
if (matchesPolicySplit(node)) {
|
|
1871
|
+
violations.push(
|
|
1872
|
+
createPolicyViolation(
|
|
1873
|
+
'portability',
|
|
1874
|
+
path,
|
|
1875
|
+
'sources split arrived text before trimming each line',
|
|
1876
|
+
node,
|
|
1877
|
+
),
|
|
1878
|
+
)
|
|
1879
|
+
}
|
|
1880
|
+
if (matchesPolicyTerminator(node) || importsPolicyTerminator(node)) {
|
|
1881
|
+
violations.push(
|
|
1882
|
+
createPolicyViolation(
|
|
1883
|
+
'portability',
|
|
1884
|
+
path,
|
|
1885
|
+
'sources emit a line feed rather than the host line ending',
|
|
1886
|
+
node,
|
|
1887
|
+
),
|
|
1888
|
+
)
|
|
1889
|
+
}
|
|
1890
|
+
ts.forEachChild(node, (child) => {
|
|
1891
|
+
violations.push(...inspectPolicyEndingNode(path, child))
|
|
1892
|
+
})
|
|
1893
|
+
return violations
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
/**
|
|
1897
|
+
* Inspect one parsed source for host-specific line-ending handling.
|
|
1898
|
+
*
|
|
1899
|
+
* @param source - The path and TypeScript text to inspect.
|
|
1900
|
+
* @returns Every line-ending violation in source order.
|
|
1901
|
+
*/
|
|
1902
|
+
export function inspectPolicyEndingSource(source: PolicySource): readonly PolicyViolation[] {
|
|
1903
|
+
const path = normalizePolicyPath(source.path)
|
|
1904
|
+
const syntax = ts.createSourceFile(path, source.content, ts.ScriptTarget.Latest, true)
|
|
1905
|
+
return inspectPolicyEndingNode(path, syntax)
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
/**
|
|
1909
|
+
* Inspect every source axis the portability population covers for line-ending handling.
|
|
1910
|
+
*
|
|
1911
|
+
* @param root - The workspace root to inspect.
|
|
1912
|
+
* @returns Every line-ending violation in path and source order.
|
|
1913
|
+
*/
|
|
1914
|
+
export function inspectPolicyEndings(root: string): readonly PolicyViolation[] {
|
|
1915
|
+
const violations: PolicyViolation[] = []
|
|
1916
|
+
for (const source of readPolicySources(root, POLICY_PORTABILITY_SOURCE_GLOB)) {
|
|
1917
|
+
violations.push(...inspectPolicyEndingSource(source))
|
|
1918
|
+
}
|
|
1919
|
+
return violations
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
/**
|
|
1923
|
+
* Inspect every host portability rule across one workspace.
|
|
1924
|
+
*
|
|
1925
|
+
* @param root - The workspace root to inspect.
|
|
1926
|
+
* @returns Every rule-map, filename, manifest-script, and line-ending violation.
|
|
1927
|
+
*/
|
|
1928
|
+
export function inspectPolicyPortability(root: string): readonly PolicyViolation[] {
|
|
1929
|
+
return [
|
|
1930
|
+
...inspectPolicyRuleMap(root),
|
|
1931
|
+
...inspectPolicyFilenames(root),
|
|
1932
|
+
...inspectPolicyScripts(root),
|
|
1933
|
+
...inspectPolicyEndings(root),
|
|
1934
|
+
]
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1564
1937
|
/**
|
|
1565
1938
|
* Inspect every policy rule across one workspace.
|
|
1566
1939
|
*
|
|
1567
1940
|
* @param root - The workspace root to inspect.
|
|
1568
|
-
* @returns Every source, mirror, suppression, skill, and
|
|
1941
|
+
* @returns Every source, mirror, suppression, skill, bridge, and portability violation.
|
|
1569
1942
|
*/
|
|
1570
1943
|
export function inspectPolicyWorkspace(root: string): readonly PolicyViolation[] {
|
|
1571
1944
|
return [
|
|
@@ -1574,6 +1947,7 @@ export function inspectPolicyWorkspace(root: string): readonly PolicyViolation[]
|
|
|
1574
1947
|
...inspectPolicySuppressions(root),
|
|
1575
1948
|
...inspectSkillFamily(root),
|
|
1576
1949
|
...inspectSkillBridges(root),
|
|
1950
|
+
...inspectPolicyPortability(root),
|
|
1577
1951
|
]
|
|
1578
1952
|
}
|
|
1579
1953
|
|
|
@@ -2339,6 +2713,168 @@ export const SKILL_POLICY_EXCLUSION: PolicyControl = Object.freeze({
|
|
|
2339
2713
|
files: [{ path: '.claude/skills/bridge/SKILL.md', content: SKILL_POLICY_TEXT }],
|
|
2340
2714
|
})
|
|
2341
2715
|
|
|
2716
|
+
/**
|
|
2717
|
+
* Create root instruction text whose rule map names an explicit rule set.
|
|
2718
|
+
*
|
|
2719
|
+
* @param rules - The workspace-relative rule paths the map registers.
|
|
2720
|
+
* @returns Root instruction text carrying one rule map table.
|
|
2721
|
+
*/
|
|
2722
|
+
export function createPolicyRuleMap(rules: readonly string[]): string {
|
|
2723
|
+
return (
|
|
2724
|
+
[
|
|
2725
|
+
'# Fixture instructions',
|
|
2726
|
+
'',
|
|
2727
|
+
POLICY_RULE_MAP_HEADING,
|
|
2728
|
+
'',
|
|
2729
|
+
'| Rule | Governs |',
|
|
2730
|
+
'| ---- | ------- |',
|
|
2731
|
+
...rules.map((rule) => `| \`${rule}\` | Fixture rows |`),
|
|
2732
|
+
].join('\n') + '\n'
|
|
2733
|
+
)
|
|
2734
|
+
}
|
|
2735
|
+
|
|
2736
|
+
/** Physical controls for every rule-map parity assertion the workspace route reaches. */
|
|
2737
|
+
export const RULES_POLICY_CONTROLS: readonly PolicyControl[] = Object.freeze([
|
|
2738
|
+
{
|
|
2739
|
+
label: 'rejects a rule file the rule map omits',
|
|
2740
|
+
membership: 'Markdown files directly beneath .claude/rules',
|
|
2741
|
+
rule: 'rules',
|
|
2742
|
+
message: 'the rule map names every rule file',
|
|
2743
|
+
files: [
|
|
2744
|
+
{ path: POLICY_RULE_MAP_FILE, content: createPolicyRuleMap([]) },
|
|
2745
|
+
{ path: `${POLICY_RULE_ROOT}/sample.md`, content: '# Sample\n' },
|
|
2746
|
+
],
|
|
2747
|
+
},
|
|
2748
|
+
{
|
|
2749
|
+
label: 'rejects a rule-map row that resolves to nothing',
|
|
2750
|
+
membership: 'backticked first cells in the rule map table',
|
|
2751
|
+
rule: 'rules',
|
|
2752
|
+
message: 'every rule-map row resolves to a rule file',
|
|
2753
|
+
files: [
|
|
2754
|
+
{
|
|
2755
|
+
path: POLICY_RULE_MAP_FILE,
|
|
2756
|
+
content: createPolicyRuleMap([
|
|
2757
|
+
`${POLICY_RULE_ROOT}/sample.md`,
|
|
2758
|
+
`${POLICY_RULE_ROOT}/missing.md`,
|
|
2759
|
+
]),
|
|
2760
|
+
},
|
|
2761
|
+
{ path: `${POLICY_RULE_ROOT}/sample.md`, content: '# Sample\n' },
|
|
2762
|
+
],
|
|
2763
|
+
},
|
|
2764
|
+
])
|
|
2765
|
+
|
|
2766
|
+
/** Physical controls for every portability assertion the workspace route reaches. */
|
|
2767
|
+
export const PORTABILITY_POLICY_CONTROLS: readonly PolicyControl[] = Object.freeze([
|
|
2768
|
+
{
|
|
2769
|
+
label: 'rejects a reserved device name',
|
|
2770
|
+
membership: 'path segments in the workspace-authored path population',
|
|
2771
|
+
rule: 'portability',
|
|
2772
|
+
message: 'path segments avoid the names Windows reserves',
|
|
2773
|
+
files: [{ path: 'src/worker/con.ts', content: '' }],
|
|
2774
|
+
},
|
|
2775
|
+
{
|
|
2776
|
+
label: 'rejects a segment that ends with a dot',
|
|
2777
|
+
membership: 'path segments in the workspace-authored path population',
|
|
2778
|
+
rule: 'portability',
|
|
2779
|
+
message: 'path segments end with neither a dot nor a space',
|
|
2780
|
+
files: [{ path: 'src/worker/helpers.ts.', content: '' }],
|
|
2781
|
+
},
|
|
2782
|
+
{
|
|
2783
|
+
label: 'rejects a segment that ends with a space',
|
|
2784
|
+
membership: 'path segments in the workspace-authored path population',
|
|
2785
|
+
rule: 'portability',
|
|
2786
|
+
message: 'path segments end with neither a dot nor a space',
|
|
2787
|
+
files: [{ path: 'src/worker/helpers.ts ', content: '' }],
|
|
2788
|
+
},
|
|
2789
|
+
{
|
|
2790
|
+
label: 'rejects a shell script named by a manifest script',
|
|
2791
|
+
membership: 'string values beneath the manifest scripts record',
|
|
2792
|
+
rule: 'portability',
|
|
2793
|
+
message: 'manifest scripts name no .sh file: prepare',
|
|
2794
|
+
files: [
|
|
2795
|
+
{
|
|
2796
|
+
path: POLICY_MANIFEST_FILE,
|
|
2797
|
+
content: '{\n\t"scripts": {\n\t\t"prepare": "bash scripts/prepare.sh"\n\t}\n}\n',
|
|
2798
|
+
},
|
|
2799
|
+
],
|
|
2800
|
+
},
|
|
2801
|
+
{
|
|
2802
|
+
label: 'rejects a payload trimmed before it is split',
|
|
2803
|
+
membership: 'split calls carrying a line-feed string literal in the parsed source axes',
|
|
2804
|
+
rule: 'portability',
|
|
2805
|
+
message: 'sources split arrived text before trimming each line',
|
|
2806
|
+
files: [
|
|
2807
|
+
{
|
|
2808
|
+
path: 'src/worker/helpers.ts',
|
|
2809
|
+
content:
|
|
2810
|
+
"export function readLines(text: string): readonly string[] {\n\treturn text.trim().split('\\n')\n}\n",
|
|
2811
|
+
},
|
|
2812
|
+
],
|
|
2813
|
+
},
|
|
2814
|
+
{
|
|
2815
|
+
label: 'rejects a read of the host line ending',
|
|
2816
|
+
membership: 'EOL member reads on a binding named os in the parsed source axes',
|
|
2817
|
+
rule: 'portability',
|
|
2818
|
+
message: 'sources emit a line feed rather than the host line ending',
|
|
2819
|
+
files: [
|
|
2820
|
+
{
|
|
2821
|
+
path: 'configs/helpers.ts',
|
|
2822
|
+
content:
|
|
2823
|
+
"import * as os from 'node:os'\nexport function endLine(): string {\n\treturn os.EOL\n}\n",
|
|
2824
|
+
},
|
|
2825
|
+
],
|
|
2826
|
+
},
|
|
2827
|
+
{
|
|
2828
|
+
label: 'rejects an EOL import from node:os',
|
|
2829
|
+
membership: 'named import specifiers from node:os in the parsed source axes',
|
|
2830
|
+
rule: 'portability',
|
|
2831
|
+
message: 'sources emit a line feed rather than the host line ending',
|
|
2832
|
+
files: [
|
|
2833
|
+
{
|
|
2834
|
+
path: 'configs/helpers.ts',
|
|
2835
|
+
content:
|
|
2836
|
+
"import { EOL } from 'node:os'\nexport function endLine(): string {\n\treturn EOL\n}\n",
|
|
2837
|
+
},
|
|
2838
|
+
],
|
|
2839
|
+
},
|
|
2840
|
+
])
|
|
2841
|
+
|
|
2842
|
+
/** A trimmed split outside the parsed source axes, proving the population boundary. */
|
|
2843
|
+
export const PORTABILITY_POLICY_EXCLUSION: PolicyControl = Object.freeze({
|
|
2844
|
+
label: 'excludes a script module from the parsed source axes',
|
|
2845
|
+
membership: 'TypeScript modules outside the src, app, and configs axes',
|
|
2846
|
+
rule: 'portability',
|
|
2847
|
+
files: [
|
|
2848
|
+
{
|
|
2849
|
+
path: 'scripts/read.ts',
|
|
2850
|
+
content:
|
|
2851
|
+
"export function readLines(text: string): readonly string[] {\n\treturn text.trim().split('\\n')\n}\n",
|
|
2852
|
+
},
|
|
2853
|
+
],
|
|
2854
|
+
})
|
|
2855
|
+
|
|
2856
|
+
/** A locally declared line-ending constant, which the host line-ending rule leaves legal. */
|
|
2857
|
+
export const PORTABILITY_POLICY_LOCAL: PolicyControl = Object.freeze({
|
|
2858
|
+
label: 'accepts a locally declared EOL constant',
|
|
2859
|
+
membership: 'EOL identifiers that neither read a binding named os nor import from node:os',
|
|
2860
|
+
rule: 'portability',
|
|
2861
|
+
files: [{ path: 'src/worker/constants.ts', content: "export const EOL = '\\n'\n" }],
|
|
2862
|
+
})
|
|
2863
|
+
|
|
2864
|
+
/** A split on the host-independent line-ending pattern, which the split rule leaves legal. */
|
|
2865
|
+
export const PORTABILITY_POLICY_SPLIT: PolicyControl = Object.freeze({
|
|
2866
|
+
label: 'accepts a split on the line-ending pattern',
|
|
2867
|
+
membership: 'split calls whose argument is not a line-feed string literal',
|
|
2868
|
+
rule: 'portability',
|
|
2869
|
+
files: [
|
|
2870
|
+
{
|
|
2871
|
+
path: 'src/worker/helpers.ts',
|
|
2872
|
+
content:
|
|
2873
|
+
'export function readLines(text: string): readonly string[] {\n\treturn text.trim().split(/\\r\\n|\\n/u)\n}\n',
|
|
2874
|
+
},
|
|
2875
|
+
],
|
|
2876
|
+
})
|
|
2877
|
+
|
|
2342
2878
|
/** A differently shaped workspace with app, browser, and worker environments but no core. */
|
|
2343
2879
|
export const GENERIC_POLICY_SOURCES: readonly PolicySource[] = Object.freeze([
|
|
2344
2880
|
{
|
package/dist/src/core/index.cjs
CHANGED
|
@@ -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.
|
|
7
|
+
version: "0.0.55",
|
|
8
8
|
description: "Scaffold workspaces with five commands: new, audit, repair, catalog, and overwrite.",
|
|
9
9
|
keywords: [
|
|
10
10
|
"audit",
|
|
@@ -89,7 +89,7 @@ var package_default = {
|
|
|
89
89
|
"prepublishOnly": "npm run format:check && npm run lint:check && npm run check && npm run build && npm test && npm run test:distribution -- --mode release"
|
|
90
90
|
},
|
|
91
91
|
dependencies: {
|
|
92
|
-
"@orkestrel/console": "^0.0.
|
|
92
|
+
"@orkestrel/console": "^0.0.11",
|
|
93
93
|
"@orkestrel/contract": "^0.0.13",
|
|
94
94
|
"@orkestrel/emitter": "^0.0.8",
|
|
95
95
|
"@orkestrel/markdown": "^0.0.12",
|
|
@@ -100,9 +100,9 @@ var package_default = {
|
|
|
100
100
|
"@microsoft/api-extractor": "^7.59.0",
|
|
101
101
|
"@orkestrel/guide": "^0.0.14",
|
|
102
102
|
"@orkestrel/html": "^0.0.7",
|
|
103
|
-
"@orkestrel/probe": "^0.0.
|
|
103
|
+
"@orkestrel/probe": "^0.0.9",
|
|
104
104
|
"@orkestrel/test": "^0.0.11",
|
|
105
|
-
"@types/node": "^26.
|
|
105
|
+
"@types/node": "^26.4.0",
|
|
106
106
|
"@vitest/browser-playwright": "^4.1.11",
|
|
107
107
|
"oxfmt": "^0.65.0",
|
|
108
108
|
"oxlint": "^1.80.0",
|