@orkestrel/scaffold 0.0.2 → 0.0.3

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 (78) hide show
  1. package/README.md +29 -15
  2. package/dist/bin/scaffold.js +1162 -1200
  3. package/dist/bin/scaffold.js.map +1 -1
  4. package/dist/host/AGENTS.md +124 -925
  5. package/dist/host/CLAUDE.md +260 -495
  6. package/dist/host/agents/skills/orkestrel-align-packages/SKILL.md +53 -0
  7. package/dist/host/agents/skills/orkestrel-align-packages/agents/openai.yaml +4 -0
  8. package/dist/host/agents/skills/orkestrel-align-packages/references/fleet.md +50 -0
  9. package/dist/host/agents/skills/orkestrel-align-packages/references/integration.md +56 -0
  10. package/dist/host/agents/skills/orkestrel-build-application/SKILL.md +64 -0
  11. package/dist/host/agents/skills/orkestrel-build-application/agents/openai.yaml +4 -0
  12. package/dist/host/agents/skills/orkestrel-build-application/references/application.md +129 -0
  13. package/dist/host/agents/skills/orkestrel-harden-package/SKILL.md +64 -0
  14. package/dist/host/agents/skills/orkestrel-harden-package/agents/openai.yaml +4 -0
  15. package/dist/host/agents/skills/orkestrel-harden-package/references/centralization.md +85 -0
  16. package/dist/host/agents/skills/orkestrel-harden-package/references/contract.md +57 -0
  17. package/dist/host/agents/skills/orkestrel-harden-package/references/hardening.md +93 -0
  18. package/dist/host/agents/skills/orkestrel-harden-package/references/research.md +61 -0
  19. package/dist/host/claude/agents/application.md +32 -0
  20. package/dist/host/claude/agents/builder.md +12 -7
  21. package/dist/host/claude/agents/checker.md +15 -8
  22. package/dist/host/claude/agents/codex.md +40 -0
  23. package/dist/host/claude/agents/grok.md +20 -37
  24. package/dist/host/claude/agents/orkestrel.md +89 -223
  25. package/dist/host/claude/agents/planner.md +14 -34
  26. package/dist/host/claude/agents/reviewer.md +39 -24
  27. package/dist/host/claude/agents/verifier.md +15 -10
  28. package/dist/host/claude/rules/application.md +57 -0
  29. package/dist/host/claude/rules/architecture.md +179 -0
  30. package/dist/host/claude/rules/browser.md +28 -0
  31. package/dist/host/claude/rules/documentation.md +64 -0
  32. package/dist/host/claude/rules/names.md +209 -0
  33. package/dist/host/claude/rules/patterns.md +130 -0
  34. package/dist/host/claude/rules/quality.md +45 -0
  35. package/dist/host/claude/rules/styles.md +64 -0
  36. package/dist/host/claude/rules/tests.md +123 -0
  37. package/dist/host/claude/rules/typescript.md +78 -0
  38. package/dist/host/claude/rules/workspace.md +180 -0
  39. package/dist/host/claude/settings.json +116 -0
  40. package/dist/host/claude/skills/orkestrel-align-packages/SKILL.md +10 -0
  41. package/dist/host/claude/skills/orkestrel-build-application/SKILL.md +12 -0
  42. package/dist/host/claude/skills/orkestrel-harden-package/SKILL.md +10 -0
  43. package/dist/host/codex/agents/analyst.toml +15 -0
  44. package/dist/host/codex/agents/application.toml +25 -0
  45. package/dist/host/codex/agents/builder.toml +24 -0
  46. package/dist/host/codex/agents/checker.toml +15 -0
  47. package/dist/host/codex/agents/grok.toml +17 -0
  48. package/dist/host/codex/agents/implementer.toml +17 -0
  49. package/dist/host/codex/agents/orkestrel.toml +16 -0
  50. package/dist/host/codex/agents/planner.toml +20 -0
  51. package/dist/host/codex/agents/reviewer.toml +18 -0
  52. package/dist/host/codex/agents/verifier.toml +14 -0
  53. package/dist/host/codex/config.toml +64 -0
  54. package/dist/host/dotfiles/oxlintrc.json +307 -1
  55. package/dist/host/guides/src/scaffold.md +2059 -2007
  56. package/dist/host/manifest.json +355 -137
  57. package/dist/host/scripts/codex.sh +49 -0
  58. package/dist/host/scripts/cursor.sh +33 -47
  59. package/dist/host/scripts/deps.sh +34 -16
  60. package/dist/host/scripts/ollama.sh +6 -149
  61. package/dist/host/tests/setupPolicy.ts +354 -0
  62. package/dist/src/core/index.cjs +6600 -1199
  63. package/dist/src/core/index.cjs.map +1 -1
  64. package/dist/src/core/index.d.cts +801 -266
  65. package/dist/src/core/index.d.ts +801 -266
  66. package/dist/src/core/index.js +6518 -1191
  67. package/dist/src/core/index.js.map +1 -1
  68. package/dist/src/server/index.cjs +3132 -593
  69. package/dist/src/server/index.cjs.map +1 -1
  70. package/dist/src/server/index.d.cts +643 -95
  71. package/dist/src/server/index.d.ts +643 -95
  72. package/dist/src/server/index.js +3054 -594
  73. package/dist/src/server/index.js.map +1 -1
  74. package/package.json +15 -12
  75. package/dist/host/claude/agents/composer.md +0 -64
  76. package/dist/host/claude/agents/researcher.md +0 -38
  77. package/dist/host/claude/agents/scout.md +0 -35
  78. package/dist/host/github/workflows/ci.yml +0 -64
@@ -1,11 +1,10 @@
1
1
  #!/bin/bash
2
2
  # ============================================================================
3
- # scripts/deps.sh — SessionStart hook: project dependencies ONLY
3
+ # scripts/deps.sh — SessionStart hook: reproducible project dependencies
4
4
  # ----------------------------------------------------------------------------
5
- # Runs in every cloud environment (the setup script never sees the repo
6
- # checkout; $CLAUDE_PROJECT_DIR here always points at the real one).
7
- # SessionStart stdout is injected into Claude's context, so this script prints
8
- # ONE meaningful line and sends the npm noise to /tmp/deps.log.
5
+ # Runs only in Claude Code's remote environment. The lockfile digest marker
6
+ # prevents a resumed session from treating stale node_modules as a valid
7
+ # installation after package-lock.json changes.
9
8
  # ============================================================================
10
9
 
11
10
  if [ "${CLAUDE_CODE_REMOTE:-}" != "true" ]; then
@@ -17,22 +16,41 @@ cd "$CLAUDE_PROJECT_DIR" || exit 0
17
16
  if [ ! -f package.json ]; then
18
17
  exit 0
19
18
  fi
20
- if [ -d node_modules ]; then
21
- echo "deps.sh: dependencies already present (resumed session) — skipped."
19
+
20
+ LOCK_MARKER="node_modules/.orkestrel-lock.sha256"
21
+ DEPS_LOG="$(mktemp /tmp/orkestrel-deps.XXXXXX)" || {
22
+ echo "deps.sh: could not create a private dependency log — skipped."
22
23
  exit 0
23
- fi
24
+ }
25
+ chmod 600 "$DEPS_LOG"
26
+
27
+ cleanup_deps_log() {
28
+ rm -f -- "$DEPS_LOG"
29
+ }
30
+
31
+ trap cleanup_deps_log EXIT
24
32
 
25
33
  if [ -f package-lock.json ]; then
26
- if npm ci >/tmp/deps.log 2>&1; then
27
- echo "deps.sh: dependencies installed (npm ci)."
28
- else
29
- echo "deps.sh: npm ci FAILED — see /tmp/deps.log before building or testing."
34
+ LOCK_HASH="$(node -e "process.stdout.write(require('node:crypto').createHash('sha256').update(require('node:fs').readFileSync('package-lock.json')).digest('hex'))" 2>"$DEPS_LOG")"
35
+
36
+ if [ -n "$LOCK_HASH" ] &&
37
+ [ -d node_modules ] &&
38
+ [ -f "$LOCK_MARKER" ] &&
39
+ [ "$(tr -d '\r\n' <"$LOCK_MARKER")" = "$LOCK_HASH" ]; then
40
+ echo "deps.sh: dependencies match package-lock.json — skipped."
41
+ exit 0
30
42
  fi
31
- else
32
- if npm install >/tmp/deps.log 2>&1; then
33
- echo "deps.sh: no package-lock.json npm install succeeded. Commit a lockfile for reproducible installs."
43
+
44
+ if npm ci --ignore-scripts >"$DEPS_LOG" 2>&1; then
45
+ if [ -n "$LOCK_HASH" ]; then
46
+ printf '%s\n' "$LOCK_HASH" >"$LOCK_MARKER"
47
+ fi
48
+ echo "deps.sh: dependencies installed from package-lock.json (npm ci)."
34
49
  else
35
- echo "deps.sh: npm install FAILED see /tmp/deps.log before building or testing."
50
+ echo "deps.sh: npm ci failed; dependency installation remains incomplete."
36
51
  fi
52
+ else
53
+ echo "deps.sh: no package-lock.json — automatic installation refused; commit a lockfile."
37
54
  fi
55
+
38
56
  exit 0
@@ -1,163 +1,20 @@
1
1
  #!/bin/bash
2
- # ============================================================================
3
- # scripts/ollama.sh SessionStart hook: Ollama ONLY
4
- # ----------------------------------------------------------------------------
5
- # One job: bring the local-model daemon up for this session, healthy.
6
- # Project dependencies live in scripts/deps.sh; the Cursor bench check lives in
7
- # scripts/cursor.sh. All three are registered in .claude/settings.json and run
8
- # IN PARALLEL at session start, so the daemon boots while npm installs.
9
- #
10
- # 0. Guards -- the orchestration set (hooks included) is mirrored
11
- # byte-identical across every @orkestrel repo, and this repo serves
12
- # several cloud environments: exit quietly in any repo that isn't
13
- # @orkestrel/ollama, and wherever the ollama binary isn't installed.
14
- # 1. Starts `ollama serve` -- the environment cache snapshots FILES, not
15
- # running processes, so the daemon must be restarted every session.
16
- # 2. Self-heals a known container defect -- this container class advertises
17
- # AMX CPU flags but blocks AMX tile-state permissions at the kernel /
18
- # seccomp level. Ollama's dynamic CPU-backend loader picks its only
19
- # AMX-capable variant (libggml-cpu-sapphirerapids.so) on such hosts, and
20
- # llama-server then segfaults on every model load. A cheap chat probe
21
- # after the daemon comes up doubles as session warmup AND a canary for
22
- # this failure; if it fires, we disable that .so and restart the daemon
23
- # so the loader falls back to the next-best AVX-512 variant.
24
- #
25
- # SessionStart stdout is injected into Claude's context: keep stdout to terse
26
- # status lines (they tell Claude what this environment offers); diagnostics go
27
- # to stderr / /tmp/ollama.log. No `set -e`: a self-heal misfire should not
28
- # stop Ollama from coming up.
29
- # ============================================================================
2
+ # SessionStart loopback capability probe. Lifecycle stays user-owned and no
3
+ # caller-controlled endpoint is contacted or copied into model context.
30
4
 
31
- # Run only in cloud sessions. Locally you manage your own Ollama, so skip.
32
5
  if [ "${CLAUDE_CODE_REMOTE:-}" != "true" ]; then
33
6
  exit 0
34
7
  fi
35
8
 
36
- # --- 0. Guards ---------------------------------------------------------------
37
- # The daemon only serves the ollama package; every other @orkestrel repo
38
- # carries this script as part of the mirrored hook set and must skip silently.
39
- PKG_JSON="${CLAUDE_PROJECT_DIR:-$(cd "$(dirname "$0")/.." && pwd)}/package.json"
40
- if ! grep -Eq '"name"[[:space:]]*:[[:space:]]*"@orkestrel/ollama"' "$PKG_JSON" 2>/dev/null; then
41
- exit 0
42
- fi
43
9
  if ! command -v ollama >/dev/null 2>&1; then
44
- echo "ollama.sh: ollama not installed in this environment — no local model daemon."
45
- exit 0
46
- fi
47
-
48
- # Match the locations used by the setup script / environment variables.
49
- export OLLAMA_MODELS="${OLLAMA_MODELS:-/opt/ollama/models}"
50
- export OLLAMA_HOST="${OLLAMA_HOST:-127.0.0.1:11434}"
51
-
52
- # Your Copilot workflow set OLLAMA_ORIGINS="*" so browser page contexts could
53
- # call the API cross-origin. That was mainly a Playwright concern, so it is off
54
- # here. Uncomment if anything still hits Ollama from a browser:
55
- # export OLLAMA_ORIGINS="*"
56
-
57
- # --- 1. Start the daemon ----------------------------------------------------
58
- if ! curl -sf "http://${OLLAMA_HOST}/api/tags" >/dev/null 2>&1; then
59
- nohup ollama serve >/tmp/ollama.log 2>&1 &
60
- fi
61
-
62
- ollama_ready=0
63
- for i in $(seq 1 30); do
64
- if curl -sf "http://${OLLAMA_HOST}/api/tags" >/dev/null 2>&1; then
65
- echo "Ollama ready on ${OLLAMA_HOST}"
66
- ollama_ready=1
67
- break
68
- fi
69
- sleep 1
70
- done
71
-
72
- if [ "$ollama_ready" != "1" ]; then
73
- # Don't fail the session if the daemon is slow; Claude can retry mid-session.
74
- echo "Warning: Ollama did not report ready within 30s (see /tmp/ollama.log)" >&2
10
+ echo "ollama.sh: loopback capability unavailable."
75
11
  exit 0
76
12
  fi
77
13
 
78
- # --- 2. Self-heal the AMX/XTILE-blocked-container defect --------------------
79
- # See header note 2. A minimal chat call against whatever model is already
80
- # installed both warms that model for the session AND tells us whether this
81
- # container hit the AMX-tile-permission wall. If it did, disable the
82
- # AMX-only CPU backend variant and restart the daemon so the loader falls
83
- # back to AVX-512, then re-probe once to confirm the fix landed.
84
- sapphirerapids_so="/usr/local/lib/ollama/libggml-cpu-sapphirerapids.so"
85
-
86
- probe_model() {
87
- curl -sf "http://${OLLAMA_HOST}/api/tags" 2>/dev/null \
88
- | grep -o '"name":"[^"]*"' \
89
- | head -n 1 \
90
- | sed -e 's/"name":"//' -e 's/"$//'
91
- }
92
-
93
- probe_chat() {
94
- model="$1"
95
- curl -s --max-time 120 "http://${OLLAMA_HOST}/api/chat" \
96
- -d "{\"model\":\"${model}\",\"messages\":[{\"role\":\"user\",\"content\":\"hi\"}],\"stream\":false,\"think\":false,\"keep_alive\":\"30m\",\"options\":{\"num_predict\":1}}"
97
- }
98
-
99
- model="$(probe_model)"
100
- if [ -z "$model" ]; then
101
- # No model installed yet -- nothing to warm or probe against.
102
- echo "ollama.sh: no installed model found, skipping warmup probe." >&2
103
- exit 0
104
- fi
105
-
106
- probe_response="$(probe_chat "$model")"
107
-
108
- if echo "$probe_response" | grep -qi 'segmentation fault\|llama-server process has terminated'; then
109
- echo "ollama.sh: detected llama-server crash on model warmup (likely the AMX/XTILE-blocked-container defect)." >&2
110
- if [ -f "$sapphirerapids_so" ]; then
111
- echo "ollama.sh: disabling AMX-only CPU backend variant so the loader falls back to AVX-512." >&2
112
- if mv "$sapphirerapids_so" "${sapphirerapids_so}.disabled" 2>/dev/null; then
113
- echo "ollama.sh: restarting Ollama daemon to pick up the backend change." >&2
114
- ollama_pid="$(pgrep -x ollama | head -n 1)"
115
- if [ -n "$ollama_pid" ]; then
116
- kill "$ollama_pid" 2>/dev/null
117
- # Wait for the old process to actually exit before relaunching, so the
118
- # new daemon can't race the dying one for the port and the readiness
119
- # poll below can't be satisfied by the dying daemon.
120
- for i in $(seq 1 10); do
121
- if ! kill -0 "$ollama_pid" 2>/dev/null; then
122
- break
123
- fi
124
- sleep 0.5
125
- done
126
- fi
127
- nohup ollama serve >/tmp/ollama.log 2>&1 &
128
-
129
- restarted_ready=0
130
- for i in $(seq 1 30); do
131
- if curl -sf "http://${OLLAMA_HOST}/api/tags" >/dev/null 2>&1; then
132
- restarted_ready=1
133
- break
134
- fi
135
- sleep 1
136
- done
137
-
138
- if [ "$restarted_ready" = "1" ]; then
139
- retry_response="$(probe_chat "$model")"
140
- if echo "$retry_response" | grep -qi 'segmentation fault\|llama-server process has terminated'; then
141
- echo "ollama.sh: self-heal restart complete, but the retry probe still failed. See /tmp/ollama.log." >&2
142
- else
143
- echo "ollama.sh: self-heal succeeded -- model warm (${model}) after AVX-512 fallback."
144
- fi
145
- else
146
- echo "ollama.sh: daemon did not report ready after self-heal restart (see /tmp/ollama.log)." >&2
147
- fi
148
- else
149
- echo "ollama.sh: warning -- could not rename ${sapphirerapids_so} (permissions?). Leaving daemon as-is." >&2
150
- fi
151
- else
152
- echo "ollama.sh: warning -- AMX-only CPU backend not found at expected path; cannot self-heal automatically." >&2
153
- fi
154
- elif echo "$probe_response" | grep -q '"message"\|"content"\|"done":true'; then
155
- echo "ollama.sh: model warm (${model}) -- warmup probe succeeded."
14
+ if curl -sf --max-time 2 "http://127.0.0.1:11434/api/tags" >/dev/null 2>&1; then
15
+ echo "ollama.sh: loopback capability ready; lifecycle remains user-owned."
156
16
  else
157
- echo "ollama.sh: warning -- warmup probe returned an unexpected response (not a segfault, but no message/done payload either):" >&2
158
- echo "ollama.sh: ${probe_response}" | head -c 500 >&2
159
- echo "" >&2
160
- echo "ollama.sh: leaving daemon as-is." >&2
17
+ echo "ollama.sh: loopback capability unreachable; lifecycle remains user-owned."
161
18
  fi
162
19
 
163
20
  exit 0
@@ -0,0 +1,354 @@
1
+ import { globSync, readFileSync } from 'node:fs'
2
+ import { basename, join } from 'node:path'
3
+ import * as ts from 'typescript'
4
+
5
+ /** Centralized source modules whose top-level declarations must all be exported. */
6
+ export const CENTRAL_SOURCE_FILES: readonly string[] = Object.freeze([
7
+ 'combinators.ts',
8
+ 'cloners.ts',
9
+ 'compilers.ts',
10
+ 'constants.ts',
11
+ 'contracts.ts',
12
+ 'errors.ts',
13
+ 'factories.ts',
14
+ 'helpers.ts',
15
+ 'handlers.ts',
16
+ 'middlewares.ts',
17
+ 'parsers.ts',
18
+ 'relations.ts',
19
+ 'schemas.ts',
20
+ 'seeders.ts',
21
+ 'shapers.ts',
22
+ 'templates.ts',
23
+ 'types.ts',
24
+ 'validators.ts',
25
+ ])
26
+
27
+ /** Centralized files that own module-scope function declarations. */
28
+ export const FUNCTION_SOURCE_FILES: readonly string[] = Object.freeze([
29
+ 'combinators.ts',
30
+ 'cloners.ts',
31
+ 'compilers.ts',
32
+ 'errors.ts',
33
+ 'factories.ts',
34
+ 'handlers.ts',
35
+ 'helpers.ts',
36
+ 'middlewares.ts',
37
+ 'parsers.ts',
38
+ 'relations.ts',
39
+ 'schemas.ts',
40
+ 'seeders.ts',
41
+ 'shapers.ts',
42
+ 'validators.ts',
43
+ ])
44
+
45
+ /** Centralized files that own module-scope data declarations. */
46
+ export const DATA_SOURCE_FILES: readonly string[] = Object.freeze([
47
+ 'combinators.ts',
48
+ 'constants.ts',
49
+ 'contracts.ts',
50
+ 'relations.ts',
51
+ 'schemas.ts',
52
+ 'templates.ts',
53
+ 'validators.ts',
54
+ ])
55
+
56
+ /** One script block extracted from a Vue SFC by the official compiler. */
57
+ export interface VueScriptBlockInterface {
58
+ readonly content: string
59
+ readonly lang?: string
60
+ }
61
+
62
+ /** An injected official Vue SFC script-block extractor. */
63
+ export interface VueScriptExtractorInterface {
64
+ (path: string, content: string): readonly VueScriptBlockInterface[]
65
+ }
66
+
67
+ /** Normalize platform separators and duplicate glob segments for stable diagnostics. */
68
+ export function normalizePolicyPath(path: string): string {
69
+ return path.replaceAll('\\', '/').replace(/\/+/gu, '/')
70
+ }
71
+
72
+ /** Whether a declaration carries an explicit export modifier. */
73
+ export function hasExportModifier(node: ts.Node): boolean {
74
+ return (
75
+ ts.canHaveModifiers(node) &&
76
+ ts.getModifiers(node)?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) ===
77
+ true
78
+ )
79
+ }
80
+
81
+ /** Whether a declaration carries a specified modifier. */
82
+ export function hasModifier(node: ts.Node, modifier: ts.SyntaxKind): boolean {
83
+ return (
84
+ ts.canHaveModifiers(node) &&
85
+ ts.getModifiers(node)?.some((candidate) => candidate.kind === modifier) === true
86
+ )
87
+ }
88
+
89
+ /** Whether a module specifier uses a URL scheme other than Node's builtin namespace. */
90
+ export function isUnsupportedModuleSpecifier(specifier: string): boolean {
91
+ return /^[A-Za-z][A-Za-z0-9+.-]*:/u.test(specifier) && !specifier.startsWith('node:')
92
+ }
93
+
94
+ /** Whether an arrow/function expression is an anonymous callback passed directly as an argument. */
95
+ export function isDirectCallback(node: ts.ArrowFunction | ts.FunctionExpression): boolean {
96
+ const parent = node.parent
97
+ return (
98
+ (ts.isCallExpression(parent) || ts.isNewExpression(parent)) &&
99
+ parent.arguments?.some((argument) => argument === node) === true
100
+ )
101
+ }
102
+
103
+ /** Whether a function expression is assigned by one module-scope variable declaration. */
104
+ export function isModuleFunction(node: ts.ArrowFunction | ts.FunctionExpression): boolean {
105
+ const declaration = node.parent
106
+ const list = declaration.parent
107
+ const statement = list.parent
108
+ return (
109
+ ts.isVariableDeclaration(declaration) &&
110
+ ts.isVariableDeclarationList(list) &&
111
+ ts.isVariableStatement(statement) &&
112
+ ts.isSourceFile(statement.parent)
113
+ )
114
+ }
115
+
116
+ /** Whether a property signature belongs to a centralized interface or type alias contract. */
117
+ export function isContractProperty(node: ts.PropertySignature): boolean {
118
+ let parent: ts.Node = node.parent
119
+ while (ts.isTypeLiteralNode(parent)) parent = parent.parent
120
+ return ts.isInterfaceDeclaration(parent) || ts.isTypeAliasDeclaration(parent)
121
+ }
122
+
123
+ /** Whether the sole production triple-slash reference is the generated browser Vite contract. */
124
+ export function hasAllowedTripleSlashReference(path: string, source: ts.SourceFile): boolean {
125
+ return (
126
+ path.replaceAll('\\', '/') === 'app/browser/env.d.ts' &&
127
+ source.referencedFiles.length === 0 &&
128
+ source.libReferenceDirectives.length === 0 &&
129
+ source.typeReferenceDirectives.length === 1 &&
130
+ source.typeReferenceDirectives[0]?.fileName === 'vite/client'
131
+ )
132
+ }
133
+
134
+ /** Inspect a Vue single-file component for syntax that can bypass declared import policy. */
135
+ export function inspectVueCodingLaw(
136
+ path: string,
137
+ scripts: readonly VueScriptBlockInterface[] = [],
138
+ ): readonly string[] {
139
+ const violations: string[] = []
140
+ for (const [index, script] of scripts.entries()) {
141
+ if (
142
+ script.lang !== 'ts' &&
143
+ script.lang !== 'tsx' &&
144
+ script.lang !== 'mts' &&
145
+ script.lang !== 'cts'
146
+ ) {
147
+ violations.push(`${path}.script-${String(index)} requires a TypeScript script language`)
148
+ }
149
+ const extension =
150
+ script.lang === 'tsx' || script.lang === 'mts' || script.lang === 'cts' ? script.lang : 'ts'
151
+ violations.push(
152
+ ...inspectCodingLaw(`${path}.script-${String(index)}.${extension}`, script.content),
153
+ )
154
+ }
155
+ return violations
156
+ }
157
+
158
+ /** Add syntax-wide coding-law violations while traversing one source tree. */
159
+ export function inspectCodingNode(path: string, node: ts.Node, violations: string[]): void {
160
+ if (
161
+ ts.isAsExpression(node) ||
162
+ ts.isTypeAssertionExpression(node) ||
163
+ ts.isNonNullExpression(node)
164
+ ) {
165
+ violations.push(`${path}:${node.getStart()} forbids type/non-null assertions`)
166
+ }
167
+ if (node.kind === ts.SyntaxKind.AnyKeyword) {
168
+ violations.push(`${path}:${node.getStart()} forbids any`)
169
+ }
170
+ if (
171
+ (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) &&
172
+ node.moduleSpecifier !== undefined &&
173
+ ts.isStringLiteral(node.moduleSpecifier) &&
174
+ isUnsupportedModuleSpecifier(node.moduleSpecifier.text)
175
+ ) {
176
+ violations.push(`${path}:${node.getStart()} forbids non-Node URL module specifiers`)
177
+ }
178
+ if (
179
+ ts.isPropertySignature(node) &&
180
+ isContractProperty(node) &&
181
+ !hasModifier(node, ts.SyntaxKind.ReadonlyKeyword)
182
+ ) {
183
+ violations.push(`${path}:${node.getStart()} requires readonly contract properties`)
184
+ }
185
+ if (
186
+ ts.isCallExpression(node) &&
187
+ node.expression.kind === ts.SyntaxKind.ImportKeyword &&
188
+ (node.arguments.length !== 1 || !node.arguments.every(ts.isStringLiteral))
189
+ ) {
190
+ violations.push(
191
+ `${path}:${node.getStart()} requires dynamic imports to use string literals so import policy remains enforceable`,
192
+ )
193
+ }
194
+ if (
195
+ ts.isCallExpression(node) &&
196
+ node.expression.kind === ts.SyntaxKind.ImportKeyword &&
197
+ node.arguments.length === 1 &&
198
+ node.arguments.every(ts.isStringLiteral) &&
199
+ isUnsupportedModuleSpecifier(node.arguments[0]?.text ?? '')
200
+ ) {
201
+ violations.push(`${path}:${node.getStart()} forbids non-Node URL module specifiers`)
202
+ }
203
+ if (ts.isFunctionDeclaration(node) && !ts.isSourceFile(node.parent)) {
204
+ violations.push(`${path}:${node.getStart()} forbids nested function declarations`)
205
+ }
206
+ if (
207
+ (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) &&
208
+ !isDirectCallback(node) &&
209
+ !isModuleFunction(node)
210
+ ) {
211
+ violations.push(`${path}:${node.getStart()} forbids hidden function assignments`)
212
+ }
213
+ ts.forEachChild(node, (child) => inspectCodingNode(path, child, violations))
214
+ }
215
+
216
+ /** Inspect one TypeScript source module for repository coding-law violations. */
217
+ export function inspectCodingLaw(path: string, content: string): readonly string[] {
218
+ const violations: string[] = []
219
+ const source = ts.createSourceFile(path, content, ts.ScriptTarget.Latest, true)
220
+ const file = basename(path)
221
+
222
+ if (/\.[cm]?jsx?$/u.test(path)) {
223
+ violations.push(`${path} production modules use TypeScript source extensions`)
224
+ }
225
+ if (/@ts-(?:expect-error|ignore|nocheck)|eslint-disable|oxlint-disable/u.test(content)) {
226
+ violations.push(`${path} forbids suppression directives`)
227
+ }
228
+ if (
229
+ (source.referencedFiles.length > 0 ||
230
+ source.libReferenceDirectives.length > 0 ||
231
+ source.typeReferenceDirectives.length > 0) &&
232
+ !hasAllowedTripleSlashReference(path, source)
233
+ ) {
234
+ violations.push(`${path} forbids triple-slash references outside app/browser/env.d.ts`)
235
+ }
236
+
237
+ if (file === 'index.ts') {
238
+ for (const statement of source.statements) {
239
+ if (
240
+ !ts.isExportDeclaration(statement) ||
241
+ statement.exportClause !== undefined ||
242
+ statement.isTypeOnly ||
243
+ statement.moduleSpecifier === undefined
244
+ ) {
245
+ violations.push(`${path} barrels contain only export * declarations`)
246
+ }
247
+ }
248
+ }
249
+
250
+ for (const statement of source.statements) {
251
+ if (
252
+ (ts.isInterfaceDeclaration(statement) || ts.isTypeAliasDeclaration(statement)) &&
253
+ file !== 'types.ts'
254
+ ) {
255
+ violations.push(`${path} centralizes interfaces and type aliases in types.ts`)
256
+ }
257
+ if (
258
+ CENTRAL_SOURCE_FILES.includes(file) &&
259
+ (ts.isClassDeclaration(statement) ||
260
+ ts.isFunctionDeclaration(statement) ||
261
+ ts.isInterfaceDeclaration(statement) ||
262
+ ts.isTypeAliasDeclaration(statement) ||
263
+ ts.isVariableStatement(statement)) &&
264
+ !hasExportModifier(statement)
265
+ ) {
266
+ violations.push(`${path} exports every centralized declaration`)
267
+ }
268
+ if (ts.isFunctionDeclaration(statement) && !FUNCTION_SOURCE_FILES.includes(file)) {
269
+ violations.push(`${path} places module functions in their centralized kind file`)
270
+ }
271
+ if (ts.isVariableStatement(statement) && !DATA_SOURCE_FILES.includes(file)) {
272
+ violations.push(`${path} places module data in its centralized kind file`)
273
+ }
274
+ if (
275
+ ts.isClassDeclaration(statement) &&
276
+ file !== 'errors.ts' &&
277
+ !/^[A-Z][A-Za-z0-9]*\.ts$/u.test(file)
278
+ ) {
279
+ violations.push(`${path} places each class in its matching implementation or errors file`)
280
+ }
281
+ if (ts.isEnumDeclaration(statement) && file !== 'types.ts') {
282
+ violations.push(`${path} centralizes enum contracts in types.ts`)
283
+ }
284
+ if (file === 'constants.ts' && ts.isVariableStatement(statement)) {
285
+ if ((statement.declarationList.flags & ts.NodeFlags.Const) === 0) {
286
+ violations.push(`${path} constants.ts permits only const declarations`)
287
+ }
288
+ for (const declaration of statement.declarationList.declarations) {
289
+ if (
290
+ !ts.isIdentifier(declaration.name) ||
291
+ !/^[A-Z][A-Z0-9_]*$/u.test(declaration.name.text)
292
+ ) {
293
+ violations.push(`${path}:${declaration.getStart()} requires UPPER_SNAKE_CASE constants`)
294
+ }
295
+ if (
296
+ declaration.initializer !== undefined &&
297
+ (ts.isArrayLiteralExpression(declaration.initializer) ||
298
+ ts.isObjectLiteralExpression(declaration.initializer))
299
+ ) {
300
+ violations.push(`${path}:${declaration.getStart()} freezes collection constants`)
301
+ }
302
+ }
303
+ }
304
+ }
305
+
306
+ if (/^[A-Z][A-Za-z0-9]*\.ts$/u.test(file)) {
307
+ const classes = source.statements.filter(ts.isClassDeclaration)
308
+ const invalid = source.statements.filter(
309
+ (statement) => !ts.isImportDeclaration(statement) && !ts.isClassDeclaration(statement),
310
+ )
311
+ if (
312
+ classes.length !== 1 ||
313
+ invalid.length !== 0 ||
314
+ classes[0]?.name?.text !== file.slice(0, -3) ||
315
+ classes[0] === undefined ||
316
+ !hasExportModifier(classes[0])
317
+ ) {
318
+ violations.push(
319
+ `${path} implementation modules contain imports and one matching exported class`,
320
+ )
321
+ }
322
+ for (const member of classes[0]?.members ?? []) {
323
+ if (hasModifier(member, ts.SyntaxKind.PrivateKeyword)) {
324
+ violations.push(`${path}:${member.getStart()} uses runtime # privacy`)
325
+ }
326
+ }
327
+ }
328
+
329
+ inspectCodingNode(path, source, violations)
330
+ return violations
331
+ }
332
+
333
+ /** Inspect every production TypeScript module under one workspace. */
334
+ export function inspectCodingWorkspace(
335
+ root: string,
336
+ vueScripts?: VueScriptExtractorInterface,
337
+ ): readonly string[] {
338
+ const violations: string[] = []
339
+ for (const path of globSync('{app,src}/**/*.{cjs,cts,js,jsx,mjs,mts,ts,tsx,vue}', {
340
+ cwd: root,
341
+ })) {
342
+ const content = readFileSync(join(root, path), 'utf8')
343
+ const normalizedPath = normalizePolicyPath(path)
344
+ if (path.endsWith('.vue') && !normalizedPath.startsWith('app/browser/')) {
345
+ violations.push(`${normalizedPath} Vue components belong in app/browser`)
346
+ }
347
+ violations.push(
348
+ ...(path.endsWith('.vue')
349
+ ? inspectVueCodingLaw(normalizedPath, vueScripts?.(normalizedPath, content))
350
+ : inspectCodingLaw(normalizedPath, content)),
351
+ )
352
+ }
353
+ return violations
354
+ }