@thedecipherist/claude-packs 1.0.0 → 1.1.0

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 (148) hide show
  1. package/CHANGELOG.md +74 -0
  2. package/README.md +275 -18
  3. package/bin/claude-packs.mjs +18 -10
  4. package/configurations/base/config.json +1 -1
  5. package/configurations/cpp/config.json +2 -1
  6. package/configurations/full-stack/config.json +2 -1
  7. package/configurations/go/config.json +3 -1
  8. package/configurations/node/config.json +3 -1
  9. package/configurations/python/config.json +3 -1
  10. package/configurations/rust/config.json +106 -0
  11. package/configurations/synth-creator/config.json +1 -1
  12. package/global/hooks/lib/shell-writes.mjs +2 -293
  13. package/lib/build-queue.mjs +22 -0
  14. package/lib/claude-md.mjs +4 -2
  15. package/lib/config-cli.mjs +1 -1
  16. package/lib/coverage.mjs +10 -2
  17. package/lib/doctor.mjs +14 -3
  18. package/lib/fix-plan.mjs +2 -1
  19. package/lib/found-folders.mjs +25 -0
  20. package/lib/global.mjs +3 -3
  21. package/lib/hook-proof.mjs +24 -7
  22. package/lib/kit-command.mjs +14 -0
  23. package/lib/layout.mjs +27 -3
  24. package/lib/logs.mjs +45 -5
  25. package/lib/new.mjs +1 -1
  26. package/lib/packs.mjs +4 -2
  27. package/lib/rule-tests.mjs +42 -0
  28. package/lib/scan-detect.mjs +41 -5
  29. package/lib/scan.mjs +5 -2
  30. package/lib/suggest.mjs +16 -3
  31. package/lib/sync.mjs +5 -2
  32. package/lib/workorder.mjs +26 -0
  33. package/package.json +1 -1
  34. package/packs/audio-core/hooks/param-id-lock.mjs +59 -16
  35. package/packs/audio-core/pack.json +5 -0
  36. package/packs/audio-core/rules/a-control-is-proved-through-its-input.md +26 -0
  37. package/packs/audio-core/rules/cpu-worst-case-per-block.md +3 -1
  38. package/packs/audio-core/rules/house-control-behaviours.md +1 -1
  39. package/packs/audio-core/rules/scoped-no-denormals.md +1 -1
  40. package/packs/audio-core/rules/straight-on-and-aligned.md +1 -1
  41. package/packs/audio-core/rules/ui-matches-reference-look.md +2 -0
  42. package/packs/audio-core/skills/analyse-reference-look/SKILL.md +25 -11
  43. package/packs/audio-core/skills/prove-control/SKILL.md +2 -0
  44. package/packs/base/checks/one-walk-per-template.json +1 -1
  45. package/packs/base/hooks/code-shape.mjs +10 -6
  46. package/packs/base/hooks/dispatch-once.mjs +58 -21
  47. package/packs/base/hooks/folder-law.mjs +3 -0
  48. package/packs/base/hooks/git-guard.mjs +22 -109
  49. package/packs/base/hooks/lesson-checks.mjs +9 -1
  50. package/packs/base/hooks/lib/code-shapes.mjs +11 -6
  51. package/packs/base/hooks/lib/crash-policy.mjs +47 -0
  52. package/packs/base/hooks/lib/dup-index.mjs +6 -1
  53. package/packs/base/hooks/lib/dup.mjs +2 -2
  54. package/packs/base/hooks/lib/event-log.mjs +4 -3
  55. package/packs/base/hooks/lib/git-adds.mjs +2 -1
  56. package/packs/base/hooks/lib/git-auth.mjs +44 -0
  57. package/packs/base/hooks/lib/git-guard-selftest.mjs +93 -0
  58. package/packs/base/hooks/lib/languages.mjs +5 -2
  59. package/packs/base/hooks/lib/layout.mjs +6 -2
  60. package/packs/base/hooks/lib/making-check-selftest.mjs +3 -0
  61. package/packs/base/hooks/lib/near-copies.mjs +1 -1
  62. package/packs/base/hooks/lib/program-read.mjs +6 -3
  63. package/packs/base/hooks/lib/rust-scan.mjs +139 -0
  64. package/packs/base/hooks/lib/shape-lists.mjs +31 -0
  65. package/packs/base/hooks/lib/shape-prose.mjs +78 -0
  66. package/packs/base/hooks/lib/shape.mjs +18 -39
  67. package/packs/base/hooks/lib/turn.mjs +6 -1
  68. package/packs/base/hooks/lib/unread-writes.mjs +24 -0
  69. package/packs/base/hooks/making-check.mjs +12 -7
  70. package/packs/base/hooks/run-hooks.mjs +15 -7
  71. package/packs/base/hooks/shape-law.mjs +11 -4
  72. package/packs/base/hooks/suite-once.mjs +22 -6
  73. package/packs/base/rules/a-control-is-seen-to-fire.md +1 -1
  74. package/packs/base/rules/behaviour-lives-with-its-kind.md +1 -1
  75. package/packs/base/rules/bug-fix-starts-red.md +1 -1
  76. package/packs/base/rules/field-map-over-if-runs.md +1 -1
  77. package/packs/base/rules/instances-by-index-identities-by-id.md +2 -2
  78. package/packs/base/rules/lookup-over-if-chains.md +1 -1
  79. package/packs/base/rules/plan-and-logs.md +1 -1
  80. package/packs/base/rules/refusals-are-judged.md +1 -0
  81. package/packs/base/rules/test-the-template-once.md +2 -1
  82. package/packs/base/rules/unattended-runs.md +15 -0
  83. package/packs/base/skills/new-project/SKILL.md +23 -0
  84. package/packs/base/skills/write-a-workorder/SKILL.md +41 -0
  85. package/packs/cpp/checks/cpp-find-with-the-library.json +2 -2
  86. package/packs/go/pack.json +5 -0
  87. package/packs/hook-proof/rules/a-refusal-is-exit-2.md +2 -2
  88. package/packs/instrument-shell/rules/render-the-extremes.md +2 -2
  89. package/packs/midi/checks/position-not-integrated.json +5 -1
  90. package/packs/postgres/checks/pg-bind-values.json +33 -0
  91. package/packs/postgres/checks/pg-concurrently-in-txn.json +18 -0
  92. package/packs/postgres/checks/pg-fk-not-valid.json +18 -0
  93. package/packs/postgres/checks/pg-index-concurrently.json +18 -0
  94. package/packs/postgres/checks/pg-no-float-money.json +19 -0
  95. package/packs/postgres/checks/pg-no-money.json +18 -0
  96. package/packs/postgres/checks/pg-no-required-column.json +18 -0
  97. package/packs/postgres/checks/pg-no-volatile-default.json +18 -0
  98. package/packs/postgres/checks/pg-not-exists.json +18 -0
  99. package/packs/postgres/checks/pg-timestamptz.json +18 -0
  100. package/packs/postgres/hooks/db-wipe-guard.mjs +95 -0
  101. package/packs/postgres/pack.json +48 -0
  102. package/packs/postgres/rules/postgres-migrations-never-stop-traffic.md +28 -0
  103. package/packs/postgres/rules/postgres-one-pool-per-process.md +21 -0
  104. package/packs/postgres/rules/postgres-queries-are-null-safe-and-bounded.md +26 -0
  105. package/packs/postgres/rules/postgres-types-say-what-they-hold.md +22 -0
  106. package/packs/postgres/rules/postgres-values-are-bound.md +130 -0
  107. package/packs/postgres/skills/postgres-safe-migration/SKILL.md +45 -0
  108. package/packs/reverse-engineering/rules/addresses-name-their-space.md +1 -0
  109. package/packs/reverse-engineering/rules/reproduction-or-original.md +1 -1
  110. package/packs/reverse-engineering/skills/gather-sources/SKILL.md +1 -0
  111. package/packs/rust/checks/rust-allow-needs-reason.json +16 -0
  112. package/packs/rust/checks/rust-blocking-in-async.json +32 -0
  113. package/packs/rust/checks/rust-if-chain-is-a-match.json +32 -0
  114. package/packs/rust/checks/rust-static-mut.json +32 -0
  115. package/packs/rust/checks/rust-string-as-error.json +34 -0
  116. package/packs/rust/checks/rust-stub-left-in.json +32 -0
  117. package/packs/rust/checks/rust-unsafe-needs-safety.json +31 -0
  118. package/packs/rust/checks/rust-unwrap-in-library.json +34 -0
  119. package/packs/rust/checks/rust-wildcard-dependency.json +16 -0
  120. package/packs/rust/clippy.toml +5 -0
  121. package/packs/rust/pack.json +27 -0
  122. package/packs/rust/rules/rust-async-never-blocks.md +20 -0
  123. package/packs/rust/rules/rust-borrow-before-you-clone.md +19 -0
  124. package/packs/rust/rules/rust-errors-are-returned.md +22 -0
  125. package/packs/rust/rules/rust-lints-are-the-gate.md +23 -0
  126. package/packs/rust/rules/rust-repeats-become-templates.md +28 -0
  127. package/packs/rust/rules/rust-unsafe-is-proved.md +21 -0
  128. package/packs/rust/skills/rust-extract-template/SKILL.md +50 -0
  129. package/packs/rust/skills/rust-port/SKILL.md +22 -0
  130. package/packs/rust/skills/rust-review/SKILL.md +24 -0
  131. package/packs/rust/tools/rust-lints.toml +19 -0
  132. package/packs/sound-design/skills/bounce-a-plugin/SKILL.md +40 -0
  133. package/packs/sound-design/tools/vst3host/build.sh +73 -0
  134. package/packs/sound-design/tools/vst3host/host_events.h +101 -0
  135. package/packs/sound-design/tools/vst3host/options.h +172 -0
  136. package/packs/sound-design/tools/vst3host/render.cpp +245 -0
  137. package/packs/sound-design/tools/vst3host/session.h +241 -0
  138. package/packs/synth-creator/hooks/build-guard.mjs +7 -7
  139. package/packs/synth-creator/hooks/protect-vendor.mjs +2 -1
  140. package/packs/synth-creator/hooks/stage-guard.mjs +1 -1
  141. package/packs/synth-creator/rules/timing-from-synthcore.md +1 -1
  142. package/packs/synth-creator/skills/build-through-the-queue/SKILL.md +4 -2
  143. package/packs/synth-creator/tools/buildq.mjs +74 -0
  144. package/prompts/workorder/PROMPT.md +53 -0
  145. package/prompts/workorder/run.sh +36 -0
  146. package/prompts/workorder/workorder.md +59 -0
  147. package/global/hooks/lib/program-read.mjs +0 -196
  148. package/global/hooks/lib/program-writes.mjs +0 -177
@@ -1,16 +1,20 @@
1
1
  #!/usr/bin/env node
2
- // Locks parameter IDs: once seen, an ID may never disappear from the source.
2
+ // Locks parameter IDs: once seen, an ID may never disappear from the source. The lock starts itself at the first id it sees (KF-15).
3
3
  // node param-id-lock.mjs --init create .claude/param-ids.lock.json from the current repo
4
+ // node param-id-lock.mjs --status exit 1, saying why, when the unit has parameters and the lock holds no ids (doctor runs it)
4
5
  // (as PostToolUse hook) fail (exit 2) if a locked ID is gone; add new IDs to the lock. Every route that
5
6
  // writes is judged (Write, Edit, MultiEdit, NotebookEdit, and the shell's >, >>, tee,
6
7
  // sed -i, cp, mv, rm, a heredoc; lib/writes.mjs): an id deleted by `sed -i` is gone too.
7
8
  // node param-id-lock.mjs --selftest proves it
8
9
  // Where IDs are read: .claude/param-ids.config.json, else layout.json "paramIds", else JUCE
9
10
  // ParameterID / AudioParameter* string literals. Both forms: { "extensions": "<regex>", "patterns": ["<regex with one group>"] }.
11
+ // A pattern is a JavaScript regex, with ^ and $ matching at each line. One that cannot compile is reported by name, never a crash (KF-18).
12
+ // The project's build output (layout.json "buildOutput") is never read: ids in a library's sources are not the unit's (KF-15).
10
13
  // A file is judged in its own checkout: an agent worktree is scanned, and keeps its lock, on its own.
11
14
  import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
12
15
  import { dirname, join, relative } from 'node:path';
13
16
  import { fileURLToPath } from 'node:url';
17
+ import { matchesAny } from './lib/glob.mjs';
14
18
  import { readLayout } from './lib/layout.mjs';
15
19
  import { checkoutRoot, claudeRoot, insideProject } from './lib/project.mjs';
16
20
  import { writesOf } from './lib/writes.mjs';
@@ -19,28 +23,41 @@ import { asHook, isMain } from './lib/hook-log.mjs';
19
23
 
20
24
  const ROOT = process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
21
25
  // .claude is skipped so the worktrees under .claude/worktrees/ never stand in for this checkout's code.
22
- const SKIP_DIRS = /(^|\/)(build|Builds|JuceLibraryCode|JUCE|node_modules|\.git|\.claude|cmake-build[^/]*)(\/|$)/;
26
+ const SKIP_DIRS = /^out(\/|$)|(^|\/)(build|Builds|_deps|JuceLibraryCode|JUCE|node_modules|\.git|\.claude|cmake-build[^/]*)(\/|$)/;
27
+ const PARAMETER_WORDS = /\b(?:ParameterID|AudioParameter(?:Float|Int|Bool|Choice)|AudioProcessorValueTreeState|addParameter)\b/;
28
+ const CONFIG = join('.claude', 'param-ids.config.json');
23
29
 
24
30
  const DEFAULT_PATTERNS = [
25
31
  'ParameterID\\s*(?:[A-Za-z_]\\w*\\s*)?[({]\\s*"([^"]+)"',
26
32
  'AudioParameter(?:Float|Int|Bool|Choice)\\s*>?\\s*\\(\\s*"([^"]+)"',
27
33
  ];
28
34
 
35
+ // The settings, or { error } saying what cannot be read and where.
29
36
  function loadConfig(root) {
30
- const file = join(root, '.claude', 'param-ids.config.json');
31
- const cfg = existsSync(file) ? JSON.parse(readFileSync(file, 'utf8')) : readLayout(root).paramIds ?? {};
32
- return {
33
- extensions: new RegExp(cfg.extensions ?? '\\.(cpp|h|hpp|cc|mm)$'),
34
- patterns: (cfg.patterns ?? DEFAULT_PATTERNS).map((p) => new RegExp(p, 'g')),
35
- };
37
+ const file = join(root, CONFIG);
38
+ try {
39
+ const cfg = existsSync(file) ? JSON.parse(readFileSync(file, 'utf8')) : readLayout(root).paramIds ?? {};
40
+ return {
41
+ extensions: new RegExp(cfg.extensions ?? '\\.(cpp|h|hpp|cc|mm)$'),
42
+ patterns: (cfg.patterns ?? DEFAULT_PATTERNS).map((p) => new RegExp(p, 'gm')),
43
+ };
44
+ } catch (e) {
45
+ return { error: `param-id-lock could not read its settings (${CONFIG}, or layout.json "paramIds"), so no parameter id was checked: ${e.message}\n` };
46
+ }
36
47
  }
37
48
 
38
- function collectIds(root, { extensions, patterns }) {
39
- const ids = new Set();
49
+ function* sourcesOf(root, extensions) {
50
+ const output = readLayout(root).buildOutput ?? [];
40
51
  for (const entry of readdirSync(root, { recursive: true })) {
41
52
  const rel = String(entry);
42
- if (SKIP_DIRS.test(rel) || !extensions.test(rel)) continue;
43
- const text = readFileSync(join(root, rel), 'utf8');
53
+ if (!SKIP_DIRS.test(rel) && extensions.test(rel) && !matchesAny(rel, output)) yield join(root, rel);
54
+ }
55
+ }
56
+
57
+ function collectIds(root, { extensions, patterns }) {
58
+ const ids = new Set();
59
+ for (const file of sourcesOf(root, extensions)) {
60
+ const text = readFileSync(file, 'utf8');
44
61
  for (const re of patterns) {
45
62
  re.lastIndex = 0;
46
63
  let m;
@@ -52,10 +69,27 @@ function collectIds(root, { extensions, patterns }) {
52
69
 
53
70
  const lockFile = (root) => join(root, '.claude', 'param-ids.lock.json');
54
71
  const save = (root, ids) => writeFileSync(lockFile(root), JSON.stringify([...ids].sort(), null, 2) + '\n');
72
+ const lockedIds = (root) => (existsSync(lockFile(root)) ? JSON.parse(readFileSync(lockFile(root), 'utf8')) : []);
73
+
74
+ // KF-15: ids built from a table or a generator are invisible to the default patterns, and a lock with nothing in it protects nothing.
75
+ function status(root) {
76
+ const config = loadConfig(root);
77
+ if (config.error) { console.log(config.error.trim()); return 1; }
78
+ if (lockedIds(root).length || collectIds(root, config).size) return 0;
79
+ for (const file of sourcesOf(root, config.extensions)) {
80
+ if (!PARAMETER_WORDS.test(readFileSync(file, 'utf8'))) continue;
81
+ console.log(`param-id-lock: ${relative(root, file)} makes parameters and the lock holds no ids, so it protects nothing. ` +
82
+ `Say where the ids are in ${CONFIG} ("extensions", "patterns"), then \`node .claude/hooks/param-id-lock.mjs --init\`.`);
83
+ return 1;
84
+ }
85
+ return 0;
86
+ }
55
87
 
56
88
  function main(text) {
57
89
  if (process.argv.includes('--init')) {
58
- const ids = collectIds(ROOT, loadConfig(ROOT));
90
+ const config = loadConfig(ROOT);
91
+ if (config.error) { process.stderr.write(config.error); return 2; }
92
+ const ids = collectIds(ROOT, config);
59
93
  save(ROOT, ids);
60
94
  console.log(`Locked ${ids.size} parameter IDs in ${relative(ROOT, lockFile(ROOT))}`);
61
95
  return 0;
@@ -74,6 +108,7 @@ function main(text) {
74
108
  const root = file ? checkoutRoot(file) : ROOT;
75
109
  if (seen.has(root)) continue;
76
110
  const config = loadConfig(file ? claudeRoot(file) : ROOT);
111
+ if (config.error) { process.stderr.write(config.error); return 2; }
77
112
  if (file && !config.extensions.test(file)) continue; // not a file ids are written in
78
113
  seen.add(root);
79
114
  const code = judge(root, config);
@@ -84,8 +119,11 @@ function main(text) {
84
119
 
85
120
  // The checkout's own lock only: main's lock may hold ids a worktree branch never had.
86
121
  function judge(root, config) {
87
- if (!existsSync(lockFile(root))) return 0;
88
122
  const current = collectIds(root, config);
123
+ if (!existsSync(lockFile(root))) {
124
+ if (current.size) save(root, current);
125
+ return 0;
126
+ }
89
127
 
90
128
  const locked = new Set(JSON.parse(readFileSync(lockFile(root), 'utf8')));
91
129
  const missing = [...locked].filter((id) => !current.has(id));
@@ -108,7 +146,7 @@ function selftest() {
108
146
  const hook = fileURLToPath(import.meta.url);
109
147
  const params = (...ids) => ids.map((id) => `auto p_${id} = juce::ParameterID { "${id}", 1 };`).join('\n') + '\n';
110
148
  const dir = scratchProject({ 'src/params/Params.h': params('cutoff', 'res') }, { folders: {}, concepts: {} });
111
- const unlocked = scratchProject({ 'src/params/Params.h': params('cutoff') });
149
+ const unlocked = scratchProject({ 'src/params/Params.h': params('cutoff') }, { folders: {}, concepts: {} });
112
150
  const file = join(dir, 'src/params/Params.h');
113
151
  const post = (text) => (put(file, text), runHook(hook, dir, writeOf(file, text)));
114
152
  const initStatus = runHook(hook, dir, null, ['--init']);
@@ -120,10 +158,15 @@ function selftest() {
120
158
  ['an id added', post(params('cutoff', 'res', 'drive')), 0],
121
159
  ['the new id joins the lock', locked().includes('drive'), true],
122
160
  ['a project with no lock', runHook(hook, unlocked, writeOf(join(unlocked, 'src/params/Params.h'))), 0],
161
+ ['the first id seen starts the lock', existsSync(join(unlocked, '.claude/param-ids.lock.json')), true],
123
162
  ['another project is not ours', runHook(hook, dir, writeOf(join(dirname(dir), 'elsewhere/Params.h'))), 0],
124
163
  ];
125
164
  return verdicts('param-id-lock', cases);
126
165
  }
127
166
 
128
167
  export const hook = main; // run-hooks.mjs calls it with the event's text (KWB-24)
129
- if (isMain(import.meta.url)) process.exit(process.argv.includes('--selftest') ? selftest() : process.argv.includes('--init') ? main('') : await asHook('param-id-lock', main));
168
+ const BY_HAND = { '--selftest': selftest, '--status': () => status(ROOT), '--init': () => main('') };
169
+ if (isMain(import.meta.url)) {
170
+ const byHand = Object.keys(BY_HAND).find((flag) => process.argv.includes(flag));
171
+ process.exit(byHand ? BY_HAND[byHand]() : await asHook('param-id-lock', main));
172
+ }
@@ -69,6 +69,11 @@
69
69
  "ui": [
70
70
  "app"
71
71
  ],
72
+ "look-analysis": [
73
+ "design/**",
74
+ "tools/*panel*",
75
+ "tools/*look*"
76
+ ],
72
77
  "bench": [
73
78
  "bench"
74
79
  ],
@@ -0,0 +1,26 @@
1
+ ---
2
+ # concepts: ui
3
+ paths:
4
+ - "placeholder"
5
+ ---
6
+
7
+ # A control is proved through its own input path
8
+
9
+ A gate that writes the parameter proves the parameter, never the control. On the MPX 500 every control passed such a gate, while a knob's mouse handling could be compiled out with its table row, class and binding all intact. Driving the real editor found five real bugs: the host's first sync written over the loaded program, a double click that did not go back, small wheel movements lost on every knob, an LED that never lit, and a drag thrown backwards by a modifier.
10
+
11
+ <constraints>
12
+ Always do:
13
+ - Drive a control through its own input path: the real component in the real editor, at its peer, so the toolkit's own hit test, click counting and drag tracking decide what receives each gesture.
14
+ - Read what the gesture moved at each end of the chain: the parameter or the machine's state, the pixels, and the audio.
15
+ - Work every gesture the control has: press, release, drag, fine drag, wheel, a small wheel movement, double click.
16
+ - Prove a press on its own: a recipe that also turns a knob moves the audio whatever the press did.
17
+ - Prove the gate can fail: with one control's input handling taken out, the gate goes red while a gate that writes the parameter stays green. Keep both results.
18
+ - Run a planted fault in each control in turn: one unhooked control leaves its row, class and binding intact, so nothing else sees it.
19
+
20
+ Never do:
21
+ - Call a control proved because its parameter moved when written, or because its row reads right.
22
+ - Call a method on the component in place of a gesture: that goes round the hit test.
23
+ </constraints>
24
+
25
+ ## Check
26
+ Test: for each control kind, a gesture sent at the real editor's peer moves its parameter, its pixels and the audio, and the same test goes red with that control's input handling compiled out.
@@ -9,6 +9,8 @@ paths:
9
9
  <constraints>
10
10
  Always do:
11
11
  - Gate on p99, p99.9 and maximum block time, plus the count of blocks over their deadline (`numSamples / sampleRate`), at 1, 8, 32 and 96 voices, with the heaviest patch and every row of the matrix moving.
12
+ - Include a program load (or any preset change) as its own case, at every sample rate: clearing delay memory on the audio thread overran every load at 96 kHz, and no steady-state block showed it.
13
+ - On a shared machine, separate the code from the scheduler. A wall clock counts what the host and hypervisor take, and thread CPU time hides it. A deterministic engine repeats its own cost on the same block, so take the least time per block over a few passes. Run a control with no engine through the same clock, and read the engine's figure against it.
12
14
  - Include digital silence and "identical left and right" as their own cases. Silence can cost more than signal (denormals, gates).
13
15
  - Label every CPU figure with what it measures (callback duty, one core, or process CPU), sample rate, block size, and whether the editor was open.
14
16
  - Attribute cost before optimising: compare editor closed vs open, bypassed vs active, silence vs signal.
@@ -23,4 +25,4 @@ Never do:
23
25
  </constraints>
24
26
 
25
27
  ## Check
26
- Benchmark test, offline render: 96 voices at 48 kHz with 64-sample blocks. Pass (house choice): p99.9 under 50% of the deadline, and zero overruns.
28
+ Benchmark test, offline render: 96 voices at 48 kHz with 64-sample blocks. Pass (house choice): p99.9 under 50% of the deadline, and zero overruns, by the least time per block where the machine is shared.
@@ -24,7 +24,7 @@ Always do:
24
24
  It never steps to the next value on a press. The ‹ › arrows beside a list may step, as their own controls.
25
25
  - Check a new or changed control against this list with the `new-panel-control` skill, and walk one control of each template through the real window in the behaviour gate.
26
26
  - Walk one control of each template in the real window, on a loaded patch and not only on INIT: it moves its value and the sound or
27
- the picture, and a choice control opens its list. Prove the rest by a read, not a walk (`test-the-template-once`): a table check that every
27
+ the picture, and a choice control opens its list. Prove the rest by a read, not a walk (`test-the-template-once`), plus one cheap pass of every control through its real input path, since a read cannot see one unhooked (twelve controls on the MPX 500 were each caught only by working them): a table check that every
28
28
  control is built from its template and its binding reaches a live parameter, which is what catches a control wired to nothing. The gate is red on a planted control that does nothing and on a planted dropdown
29
29
  that steps instead of opening.
30
30
  - When a project's reference synth (Solera, MegaSynth) does a behaviour differently, copy it and write down what was copied.
@@ -8,7 +8,7 @@ paths:
8
8
 
9
9
  <constraints>
10
10
  Always do:
11
- - Make `juce::ScopedNoDenormals noDenormals;` the first line of every `processBlock` (and `processBlockBypassed` if it processes audio).
11
+ - Make `juce::ScopedNoDenormals noDenormals;` the first line of every `processBlock` (and `processBlockBypassed` if it processes audio). `host.readTransport` (`timing-from-synthcore`) comes second: it does no DSP, and the guard must already hold.
12
12
 
13
13
  - Use `ScopedNoDenormals` on *every* thread that runs DSP: worker threads that render voices, offline renders, and test and benchmark executables. The flag belongs to one thread only.
14
14
  - Define `JUCE_DSP_ENABLE_SNAP_TO_ZERO=0` in CMake. JUCE's `JUCE_SNAP_TO_ZERO` only does anything on Intel (it's a no-op on Arm), so `juce::dsp` filters otherwise give block-size-dependent output on x86 and not on Apple Silicon.
@@ -1,5 +1,5 @@
1
1
  ---
2
- # concepts: ui
2
+ # concepts: ui, look-analysis
3
3
  ---
4
4
 
5
5
  # A UI is drawn straight on, and everything aligns: basic UX comes before the picture
@@ -12,6 +12,8 @@ Stellix's layers landed flat and without glow (B-161): the analysis measured pos
12
12
  Always do:
13
13
  - Analyse the reference into numbers before drawing anything, with the `analyse-reference-look` skill: layout, colours sampled by script, type, glow and its colour and radius, surfaces and gradients, every control's parts (track, arc, pointer, ticks, value placement), and the gaps between elements.
14
14
  - Gate the look: render off-screen at the reference's size and compare region by region (glow, colour, contrast, position, overlap) against the analysis, with a stated tolerance per measure. A knob count differs or a knob moves past tolerance: the gate fails. Plant a fault (bloom off, a value overlapping a label) and show it fail.
15
+ - Check geometry exactly, on its own, from the drawn panel: rows, columns, spacing, sizes and shared centres. A whole-picture score is a likeness, and never a pass: the MPX 500 passed at 0.868 with its knobs on four rows.
16
+ - Measure the render with the code that measured the target, and diff each planted fault against the unplanted result: an exit code proves nothing when the gate is already red.
15
17
  - Keep repeated components identical: one component, one size (cards, tabs, knobs). A component never shrinks to fit; the container scrolls.
16
18
  - Keep text readable: at least 4.5 : 1 contrast for small text, measured, on every fill it can sit on.
17
19
  - End every drawn page with the user's picture stop on the side by side. A page is not done before that.
@@ -5,17 +5,31 @@ description: Use before building or rebuilding a page, panel or layer from a ref
5
5
 
6
6
  # Analyse a reference picture into numbers
7
7
 
8
- Write one analysis file per reference, beside it in the design reference folder. Measure by script, never by eye.
8
+ Read `straight-on-and-aligned` first: it governs every decision below. Write one analysis file per reference, in the design reference folder, or in `design/analysis/` if that folder is git-ignored. Measure by script, never by eye.
9
9
 
10
- First, separate the object from the photograph. You are learning a design, to draw it flat and straight on (`straight-on-and-aligned`). List what belongs to the photograph: the camera's angle, the lens, the lighting, bends, wear and missing parts. A bend moves everything near it. Start from what is known of the object: a panel is a flat rectangle, controls of a kind are one size, what looks like a row is a row, ears mirror each other, text sits level. A measurement that disagrees with those belongs to the photograph.
10
+ First, separate the object from the photograph. You are learning a design, to draw it flat and straight on. List what belongs to the photograph: the camera's angle, the lens, the lighting, bends, wear and missing parts, and note the camera. A bend moves everything near it. Start from what is known of the object, its true proportions from a specification if one exists, and rectify before measuring:
11
11
 
12
- 1. **Layout:** every region and every control's centre and size as fractions of the picture. Then find the grid: which controls share a row or a column, which look grouped, and the one size of each kind. Snap to it, and record what the snap moved and why.
13
- 2. **Colour:** sample background, panels, borders, text levels, accents, lit and unlit states.
14
- 3. **Glow:** for every glowing part, the colour, radius and strength, measured from the falloff.
15
- 4. **Surfaces:** gradients, edge glow, bevels, shadows, textures.
16
- 5. **Type:** font, size, weight and letter spacing of every text style, and how they relate.
17
- 6. **Controls:** each kind's parts (track ring, value arc, pointer, ticks, cap, skirt, scale numbers) and where its label and value sit.
18
- 7. **Spacing:** the gaps between elements and rows, and the dividers.
19
- 8. **Unclear:** what the picture leaves open, or shows damaged: decide from the object, and say so.
12
+ - **Rectify against a known anchor:** something placed by a standard, not an edge that may be worn. Rack slots sit where EIA-310 puts them, and they took out a bias of 4 units that the panel's own edges left in.
13
+ - **When sources disagree:** rank them, and record each one's reading. A flat, frontal photograph of the real unit outranks a drawing for places, since a guide's drawing can be wrong about layout while it looks authoritative. A specification outranks both for proportions. A drawing is good for what is even by design: pitch, and the ratio of parts. Where two independent sources agree against a third, the third is the outlier. Two pictures from one shoot are one source.
20
14
 
21
- Then list the tolerances the look gate will use for each measure, and mask what is damaged. The gate checks alignment, sizes and shared centres on their own, because a whole-picture score passes while every knob is wrong. Report the analysis file's path and the list of open items.
15
+ For each measure, give **a control and a confidence**. The control is the same measurement on something whose answer is known. The confidence says how far to trust the number: a part that fills its search box exactly was not measured at all.
16
+
17
+ 1. **Layout:** every region and every control's centre and size as fractions of the picture. Then find the grid: which controls share a row or a column, which look grouped, and the one size of each kind. Snap to it, and record what the snap moved and why. A measured offset smaller than the design's own spacing is error.
18
+ 2. **Dark on dark:** a dark part on a dark panel cannot be found by a threshold. Give each one a seed box by hand, from the grid or the neighbouring parts, and measure inside it. Say which parts were seeded.
19
+ 3. **Colour:** sample background, panels, borders, text levels, accents, lit and unlit states. Read a colour against the photograph's own white and black, which takes out exposure and white balance.
20
+ 4. **Glow:** for every glowing part, the colour, radius and strength, measured from the falloff.
21
+ 5. **Surfaces:** gradients, edge glow, bevels, shadows, textures.
22
+ 6. **Type:** font, size, weight and letter spacing of every text style, and how they relate. The typeface is a stated item and a scored choice: render each candidate in the target's legends, score it against them, and record the scores and the licence. When the real face is not to be had, say so.
23
+ 7. **Controls:** each kind's parts (track ring, value arc, pointer, ticks, cap, skirt, scale numbers), where its label and value sit, and any shape's real ends: a band's end may be slanted or square, and the eye gets it wrong.
24
+ 8. **Spacing:** the gaps between elements and rows, and the dividers.
25
+ 9. **Unclear:** what the picture leaves open, or shows damaged: decide from the object, and say so.
26
+
27
+ Then write the gate:
28
+
29
+ - List the tolerances for each measure, and mask what is damaged.
30
+ - Check geometry exactly, on its own: rows, columns, spacing, sizes and shared centres, read from the drawn panel. A whole-picture score passes while every knob is wrong, so it is never a pass: it is a likeness, to read beside the rest.
31
+ - Measure the render with the same code that measured the target, so the gate compares like with like.
32
+ - Plant a fault for each thing the gate claims, and diff each planted run against the unplanted result. An exit code proves nothing: the first run of the MPX 500's gate showed three plants "failing", and two had changed nothing, because the gate was already red.
33
+ - A meter changed while the gate is red is suspect: say so, and show the thresholds stayed put.
34
+
35
+ Report the analysis file's path and the list of open items.
@@ -8,6 +8,8 @@ description: Use after adding, renaming, re-ranging or removing a control, or be
8
8
  ## Steps
9
9
  For the control's parameter ID, run each check through the real path and save the command plus its output as evidence. Use the project's own probes and tests (see CLAUDE.md, Project commands).
10
10
 
11
+ Start with the real input path (`a-control-is-proved-through-its-input`): a gesture sent at the real editor's peer moves the parameter, the pixels and the audio. A gate that writes the parameter proves none of the control. Then see it red: with this control's input handling compiled out, the gate fails while the parameter's own tests stay green.
12
+
11
13
  1. **Host:** set the parameter by ID in an offline render, at its minimum, default and maximum. The render must change, and a null test against default must fail where it should and pass where it should.
12
14
  2. **Editor:** the widget is attached to that same ID. Moving it changes the parameter, and a host-set value shows in the widget with the same text as the host's value-to-text.
13
15
  3. **Patch:** save, load and compare. The value is bit-identical, and old patches without the ID load the spec default.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "one-walk-per-template",
3
- "why": "A test that presses, clicks or drags every instance of one template repeats the same behaviour hundreds of times and proves nothing more than one press does. Walk one instance of each template and every one-off; prove the rest with a read of the table that each is built from its template and wired.",
3
+ "why": "A test that presses, clicks or drags every instance of one template repeats the same behaviour hundreds of times and proves nothing more than one press does. Walk one instance of each template and every one-off; prove the rest with a read of the table that each is built from its template and wired. One every-instance pass that only proves each is hooked through its input path is allowed, and is not this: name it so, apart from the walks.",
4
4
  "rule": ".claude/rules/test-the-template-once.md",
5
5
  "globs": [
6
6
  "**/gates/**",
@@ -27,15 +27,17 @@ function addedLines(before, after) {
27
27
  return after.split('\n').flatMap((l, i) => { const n = left.get(l) ?? 0; left.set(l, n - 1); return n > 0 ? [] : [i + 1]; });
28
28
  }
29
29
 
30
- function lineFault(lang, text, added, off, limits) {
30
+ // Every line at fault at once: one a refusal cost a whole resend for each, the largest cost the kit added on the MPX 500 (37 of its 132 refusals).
31
+ function lineFaults(lang, text, added, off, limits) {
31
32
  const code = lang.blank(text).split('\n');
32
33
  const perLine = statementsOf(text, lang).reduce((m, s) => m.set(s.line, (m.get(s.line) ?? 0) + 1), new Map());
34
+ const faults = [];
33
35
  for (const n of added.filter((x) => !off.has(x))) {
34
36
  const width = code[n - 1]?.trimEnd().length ?? 0;
35
- if (width > limits.maxLineChars) return `line ${n} is ${width} columns of code (limit ${limits.maxLineChars}). Break it where it reads.`;
36
- if ((perLine.get(n) ?? 0) > limits.maxStatementsPerLine) return `line ${n} holds ${perLine.get(n)} statements. One statement a line: a diff, a breakpoint and a coverage line then mean something, and the file's size is true.`;
37
+ if (width > limits.maxLineChars) faults.push(`line ${n} is ${width} columns of code (limit ${limits.maxLineChars}). Break it where it reads.`);
38
+ if ((perLine.get(n) ?? 0) > limits.maxStatementsPerLine) faults.push(`line ${n} holds ${perLine.get(n)} statements. One statement a line: a diff, a breakpoint and a coverage line then mean something, and the file's size is true.`);
37
39
  }
38
- return null;
40
+ return faults;
39
41
  }
40
42
 
41
43
  function functionFault(before, after, off, limits) {
@@ -57,8 +59,8 @@ function judgeWrite(w, rel) {
57
59
  if (matchesAny(rel, layout.guards?.codeShape?.exclude ?? []) || isContent(rel, layout)) return null;
58
60
  const before = w.before ?? '';
59
61
  const off = oneOffLines(w.after);
60
- const fault = lineFault(lang, w.after, addedLines(before, w.after), off, limits) ?? functionFault(functionsOf(before, ext), functionsOf(w.after, ext), off, limits);
61
- return fault ? `${fault}\nIf this one truly must stay, say so above it: \`one-off: <reason>\`.\n` : null;
62
+ const faults = [...lineFaults(lang, w.after, addedLines(before, w.after), off, limits), functionFault(functionsOf(before, ext), functionsOf(w.after, ext), off, limits)].filter(Boolean);
63
+ return faults.length ? `${faults.join('\n')}\nIf one truly must stay, say so above it: \`one-off: <reason>\`.\n` : null;
62
64
  }
63
65
 
64
66
  const main = (text) => judgeEvent(text, { name: 'code-shape', write: judgeWrite });
@@ -76,6 +78,8 @@ function selftest() {
76
78
  ['too many parameters', w('src/b.cpp', 'void f(int a, int b, int c, int d, int e, int f, int g) {\n run(a);\n}\n'), 2],
77
79
  ['a self parameter is not counted', w('src/b.py', 'def f(self, a, b, c, d, e, g):\n run(a)\n'), 0],
78
80
  ['packed statements', w('src/c.cpp', 'void f() {\n a(); b(); c();\n}\n'), 2],
81
+ ['a Python set comprehension is one statement (KF-14)', w('src/s.py', 'left = {keep(row) for row in rows} - {drop(row) for row in rows}\n'), 0],
82
+ ['a lambda in a list counts as in a call (KF-27)', w('src/l.cpp', 'void f()\n{\n judge ("a", b, [&] { return g (1); }),\n judge ("c", d, [&] { return g (2); }),\n}\n'), 0],
79
83
  ['a for header is one statement', w('src/d.cpp', 'void f() {\n for (int i = 0; i < n; i++) total += v(i);\n}\n'), 0],
80
84
  ['a wide line', w('src/e.cpp', wide), 2],
81
85
  ['a wide comment is no code', w('src/f.cpp', `// ${'word '.repeat(50)}\nint x = 1;\n`), 0],
@@ -8,6 +8,7 @@
8
8
  import { readFileSync } from 'node:fs';
9
9
  import { extname, join } from 'node:path';
10
10
  import { blankCode } from './lib/cpp-scan.mjs';
11
+ import { lookupMatches } from './lib/rust-scan.mjs';
11
12
  import { CODE, TESTS, codeFiles } from './lib/elements.mjs';
12
13
  import { matchesAny } from './lib/glob.mjs';
13
14
  import { readLayout } from './lib/layout.mjs';
@@ -17,55 +18,84 @@ import { judgeEvent } from './lib/writes.mjs';
17
18
  import { asHook, isMain } from './lib/hook-log.mjs';
18
19
 
19
20
  const SWITCH = /\bswitch\s*\(\s*([^()]*?(?:\([^()]*\))?)\s*\)\s*\{/g;
21
+ const JUDGED = new Set([...CODE, '.rs']);
22
+ const FIX = 'Give each kind its own object, table entry or class (rule: behaviour-lives-with-its-kind), and call it from both places,\n' +
23
+ 'so a new kind is one new entry instead of an edit in every switch. In Rust, a table on an enum is a method on the enum.\n';
20
24
 
21
- // The kind a switch dispatches on: the last name in its expression (args.kind -> kind, p->mode() -> mode).
22
- export function kindsSwitched(text) {
25
+ // The last word of each `case` label of the switch body that opens at `open`: `Kind::line` -> line, `'line'` -> line.
26
+ function labelsOf(code, text, open) {
27
+ let depth = 0;
28
+ let close = open;
29
+ for (; close < code.length; close += 1) {
30
+ if (code[close] === '{') depth += 1;
31
+ else if (code[close] === '}' && --depth === 0) break;
32
+ }
33
+ const labels = new Set();
34
+ for (const m of code.slice(open, close).matchAll(/\bcase\s+([^:]*(?:::[^:]*)*?)\s*:(?!:)/g)) {
35
+ const at = open + m.index + m[0].indexOf(m[1]);
36
+ labels.add(text.slice(at, at + m[1].length).replace(/['"`]/g, '').split(/::|\./).pop().trim());
37
+ }
38
+ return labels;
39
+ }
40
+
41
+ // Each switch: the kind it dispatches on, the last name in its expression (args.kind -> kind, p->mode() -> mode), and its case labels.
42
+ // Rust's `match` is held to every variant by the compiler, so only a lookup table on an enum counts there (lib/rust-scan.mjs).
43
+ export function switchesOf(text, ext = '') {
44
+ if (ext === '.rs') return lookupMatches(text);
23
45
  const out = [];
24
- for (const m of blankCode(text).matchAll(SWITCH)) {
25
- const name = m[1].replace(/\(\s*\)$/, '').split(/\?\.|\.|->|::/).pop().trim();
26
- if (/^[A-Za-z_$][\w$]{2,}$/.test(name)) out.push(name);
46
+ const code = blankCode(text);
47
+ for (const m of code.matchAll(SWITCH)) {
48
+ const kind = m[1].replace(/\(\s*\)$/, '').split(/\?\.|\.|->|::/).pop().trim();
49
+ if (/^[A-Za-z_$][\w$]{2,}$/.test(kind)) out.push({ kind, labels: labelsOf(code, text, m.index + m[0].length - 1) });
27
50
  }
28
51
  return out;
29
52
  }
30
53
 
31
- const count = (list) => list.reduce((m, k) => m.set(k, (m.get(k) ?? 0) + 1), new Map());
54
+ // One kind or two? The bare word `kind` says little: a path step and an engine block both have one (KF-35). Two switches that share a case are on one
55
+ // kind; two whose cases are all different are not; a switch with no case to read is judged by the name.
56
+ const sameKind = (a, b) => a.kind === b.kind && (!a.labels.size || !b.labels.size || [...a.labels].some((l) => b.labels.has(l)));
57
+ const count = (list) => list.reduce((m, s) => m.set(s.kind, (m.get(s.kind) ?? 0) + 1), new Map());
32
58
 
33
- // The kinds a write switches on more often than before.
34
- export function newKinds(before, after) {
35
- const was = count(kindsSwitched(before));
36
- return [...count(kindsSwitched(after))].filter(([k, n]) => n > (was.get(k) ?? 0)).map(([k]) => k);
59
+ // The switches on a kind a write switches on more often than before.
60
+ export function newSwitches(before, after, ext = '') {
61
+ const was = count(switchesOf(before, ext));
62
+ const now = switchesOf(after, ext);
63
+ const grown = new Set([...count(now)].filter(([k, n]) => n > (was.get(k) ?? 0)).map(([k]) => k));
64
+ return now.filter((s) => grown.has(s.kind));
37
65
  }
38
66
 
39
67
  // { kind -> [files] }: the other code files of the checkout that already switch on each kind.
40
- export function elsewhere(root, selfRel, kinds, layout = readLayout(root)) {
68
+ export function elsewhere(root, selfRel, switches, layout = readLayout(root)) {
41
69
  const skip = [...TESTS, ...(layout.protected ?? [])];
42
70
  const found = new Map();
43
- for (const rel of codeFiles(root, skip)) {
71
+ for (const rel of codeFiles(root, skip, JUDGED)) {
44
72
  if (rel === selfRel) continue;
45
- const there = new Set(kindsSwitched(readFileSync(join(root, rel), 'utf8')));
46
- for (const k of kinds) if (there.has(k)) found.set(k, [...(found.get(k) ?? []), rel]);
73
+ const there = switchesOf(readFileSync(join(root, rel), 'utf8'), extname(rel));
74
+ for (const s of switches) if (there.some((t) => sameKind(s, t)) && !found.get(s.kind)?.includes(rel)) found.set(s.kind, [...(found.get(s.kind) ?? []), rel]);
47
75
  }
48
76
  return found;
49
77
  }
50
78
 
51
79
  // One write: null when it adds no second place for a kind; else the refusal.
52
80
  function judgeWrite(w, rel) {
53
- if (w.op === 'delete' || w.after === null || w.after === undefined || !CODE.has(extname(w.path)) || matchesAny(rel, TESTS)) return null;
54
- const kinds = newKinds(w.before ?? '', w.after);
55
- if (kinds.length === 0) return null;
56
- const found = elsewhere(checkoutRoot(w.path), rel, kinds, readLayout(claudeRoot(w.path)));
81
+ if (w.op === 'delete' || w.after === null || w.after === undefined || !JUDGED.has(extname(w.path)) || matchesAny(rel, TESTS)) return null;
82
+ const added = newSwitches(w.before ?? '', w.after, extname(w.path)).filter((s) => !s.owned); // a method on the enum is where the table belongs
83
+ if (added.length === 0) return null;
84
+ const twice = added.find((s, i) => added.some((t, j) => i !== j && sameKind(s, t) && s.labels.size && t.labels.size));
85
+ if (twice) return `${rel} branches on "${twice.kind}" twice in this file.\n${FIX}`;
86
+ const found = elsewhere(checkoutRoot(w.path), rel, added, readLayout(claudeRoot(w.path)));
57
87
  if (found.size === 0) return null;
58
88
  const lines = [...found].map(([k, files]) => ` "${k}" is already switched on in: ${files.slice(0, 5).join(', ')}`).join('\n');
59
- return `${rel} adds a second place that branches on the same kind.\n${lines}\n` +
60
- 'Give each kind its own object, table entry or class (rule: behaviour-lives-with-its-kind), and call it from both places,\n' +
61
- 'so a new kind is one new entry instead of an edit in every switch.\n';
89
+ return `${rel} adds a second place that branches on the same kind.\n${lines}\n${FIX}`;
62
90
  }
63
91
 
64
92
  const main = (text) => judgeEvent(text, { name: 'dispatch-once', write: judgeWrite });
65
93
 
66
94
  function selftest() {
67
95
  const hook = new URL(import.meta.url).pathname;
96
+ const rustTable = (kind, name) => `pub fn ${name}(s: &${kind}) -> u8 {\n match s {\n ${kind}::Open => 1,\n ${kind}::Paid => 2,\n ${kind}::Closed => 3,\n ${kind}::Low => 4,\n ${kind}::Mid => 5,\n }\n}\n`;
68
97
  const dir = scratchProject({
98
+ 'src/state.rs': `pub enum State { Open, Paid, Closed, Low, Mid }\n${rustTable('State', 'label')}`,
69
99
  'src/render.cpp': 'float draw (Shape& s) { switch (s.mode) { case Mode::line: return 1.0f; default: return 0.0f; } }\n',
70
100
  'tests/render_test.cpp': 'void t (Shape& s) { switch (s.format) { default: break; } }\n',
71
101
  });
@@ -79,7 +109,14 @@ function selftest() {
79
109
  ['a switch in a comment or string is not a switch', w('src/note.ts', '// switch (x.mode) {\nconst s = "switch (x.mode) {";\n'), 0],
80
110
  ['the kind is the last name in the expression', w('src/args.ts', 'function f(args) { switch (args?.shape.mode) { default: } }\n'), 2],
81
111
  ['a heredoc that adds a second switch', runHook(hook, dir, bashOf("cat > src/b.cpp <<'EOF'\nvoid b (Shape& s) { switch (s.mode) { default: break; } }\nEOF", dir)), 2],
112
+ ['a kind of the same name with other cases is another kind (KF-35)', w('src/path.cpp', 'void p (Step& s) { switch (s.mode) { case Pen::up: break; } }\n'), 0],
113
+ ['one case shared is the same kind', w('src/other.cpp', 'void o (Shape& s) { switch (s.mode) { case Mode::line: break; } }\n'), 2],
114
+ ['twice in one new file', w('src/twice.cpp', 'void a (S& s) { switch (s.pen) { case Pen::up: break; } }\nvoid b (S& s) { switch (s.pen) { case Pen::up: break; } }\n'), 2],
82
115
  ['another project is not ours', runHook(hook, dir, writeOf(join(dir, '..', 'elsewhere', 'x.cpp'), 'void x (S& s) { switch (s.mode) {} }\n')), 0],
116
+ ['Rust: a second lookup table on an enum, outside it', w('src/colour.rs', rustTable('State', 'colour')), 2],
117
+ ['Rust: the enum\'s own file keeps its tables', w('src/state2.rs', `pub enum Level { Low, Mid, High }\n${rustTable('Level', 'label')}${rustTable('Level', 'rank')}`), 0],
118
+ ['Rust: a match that does work is good Rust', w('src/step.rs', 'pub fn step(s: &State) {\n match s {\n State::Open => pay(),\n State::Paid => ship(),\n State::Closed => close(),\n }\n}\n'), 0],
119
+ ['Rust: a table of true and false is a predicate', w('src/open.rs', 'pub fn open(s: &State) -> bool {\n match s {\n State::Open => true,\n State::Paid => false,\n State::Closed => false,\n }\n}\n'), 0],
83
120
  ]);
84
121
  }
85
122
 
@@ -52,6 +52,7 @@ function selftest() {
52
52
  const w = (file) => runHook(hook, dir, writeOf(join(dir, file)));
53
53
  const sh = (cmd) => runHook(hook, dir, bashOf(cmd, dir));
54
54
  const ts = scratchProject({}, { folders: { api: { path: 'api/src', purpose: 'the API' } }, concepts: {}, sourceFiles: '\\.(ts|tsx)$' });
55
+ const plain = scratchProject({}, { folders: { src: { path: 'src', purpose: 'the code' } }, concepts: {}, sourceFiles: '\\.py$' });
55
56
  return verdicts('folder-law', [
56
57
  ['TypeScript is not source where the layout does not say so', w('stray/x.ts'), 0],
57
58
  ['a layout whose sourceFiles name TypeScript: a new .ts outside its folders (KWB-37)', runHook(hook, ts, writeOf(join(ts, 'stray/x.ts'))), 2],
@@ -59,6 +60,8 @@ function selftest() {
59
60
  ['a new file in a layout folder', w('src/core/Svf.cpp'), 0],
60
61
  ['a new file outside the layout', w('stray/New.cpp'), 2],
61
62
  ['a new file loose in src/', w('src/Loose.cpp'), 2],
63
+ ['a layout that names nothing inside src/: a file in src/ (KWB-78)', runHook(hook, plain, writeOf(join(plain, 'src/app.py'))), 0],
64
+ ['and a file outside it', runHook(hook, plain, writeOf(join(plain, 'stray/app.py'))), 2],
62
65
  ['an existing file outside the layout', w('stray/Old.cpp'), 0],
63
66
  ['an edit is never a new file', runHook(hook, dir, editOf(join(dir, 'stray/New.cpp'))), 0],
64
67
  ['a doc is not source', w('stray/notes.md'), 0],