@agentsfleet/orly 0.5.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 (112) hide show
  1. package/.githooks/pre-commit +42 -0
  2. package/.githooks/pre-push +67 -0
  3. package/AGENTS.md +348 -0
  4. package/LICENSE +21 -0
  5. package/README.md +249 -0
  6. package/SOUL.md +90 -0
  7. package/audits/agents-md.md +497 -0
  8. package/audits/agents-md.sh +353 -0
  9. package/audits/cross-tier-rates.sh +105 -0
  10. package/audits/data.sh +185 -0
  11. package/audits/deinit-pairs.sh +224 -0
  12. package/audits/design-tokens.sh +143 -0
  13. package/audits/doc-read.sh +183 -0
  14. package/audits/error-codes.sh +221 -0
  15. package/audits/fixtures/clean.diff +15 -0
  16. package/audits/fixtures/dirty.diff +16 -0
  17. package/audits/lifecycle-anchors.sh +22 -0
  18. package/audits/logging.sh +217 -0
  19. package/audits/msid-ui.sh +144 -0
  20. package/audits/parity-dispatch.sh +51 -0
  21. package/audits/rule-ledger-lib.sh +264 -0
  22. package/audits/rule-ledger.sh +225 -0
  23. package/audits/rule-paths.sh +108 -0
  24. package/audits/spec-template.sh +258 -0
  25. package/audits/sql-mod.sh +102 -0
  26. package/audits/ufs.sh +303 -0
  27. package/bin/orly +26 -0
  28. package/core/operating-model.md +256 -0
  29. package/dispatch/edit_rules.md +69 -0
  30. package/dispatch/lib.sh +213 -0
  31. package/dispatch/lifecycle.md +187 -0
  32. package/dispatch/name_architecture.md +93 -0
  33. package/dispatch/verify.md +111 -0
  34. package/dispatch/write_any.md +740 -0
  35. package/dispatch/write_any.sh +49 -0
  36. package/dispatch/write_auth.md +36 -0
  37. package/dispatch/write_changelog.md +64 -0
  38. package/dispatch/write_documentation.md +45 -0
  39. package/dispatch/write_go.md +24 -0
  40. package/dispatch/write_http.md +45 -0
  41. package/dispatch/write_mdx.md +7 -0
  42. package/dispatch/write_python.md +8 -0
  43. package/dispatch/write_rust.md +38 -0
  44. package/dispatch/write_shell.md +8 -0
  45. package/dispatch/write_spec.md +105 -0
  46. package/dispatch/write_sql.md +112 -0
  47. package/dispatch/write_sql.sh +36 -0
  48. package/dispatch/write_ts_adhere_bun.md +493 -0
  49. package/dispatch/write_ts_adhere_bun.sh +37 -0
  50. package/dispatch/write_zig.md +852 -0
  51. package/dispatch/write_zig.sh +40 -0
  52. package/docs/CHANGELOG_VOICE.md +18 -0
  53. package/docs/DISPATCH_ARCHITECTURE.md +664 -0
  54. package/docs/DISPATCH_REVIEW_DISPOSITION.md +65 -0
  55. package/docs/DOCUMENTATION_RULES.md +209 -0
  56. package/docs/EXECUTE_DOC_READS.md +29 -0
  57. package/docs/HARNESS_VERIFY_OUTPUT.md +40 -0
  58. package/docs/LIFECYCLE_PATTERNS.md +316 -0
  59. package/docs/LOGGING_STANDARD.md +397 -0
  60. package/docs/ORLY_ARCHITECTURE.md +129 -0
  61. package/docs/REST_API_DESIGN_GUIDELINES.md +783 -0
  62. package/docs/RULE_ENFORCEMENT.md +41 -0
  63. package/docs/SCHEMA_CONVENTIONS.md +93 -0
  64. package/docs/TEMPLATE.md +411 -0
  65. package/docs/VERIFY_TIERS.md +57 -0
  66. package/docs/greptile-learnings/README.md +51 -0
  67. package/docs/greptile-learnings/RULES.md +1045 -0
  68. package/fixtures/registry-invalid.json +1 -0
  69. package/fixtures/registry-valid.json +18 -0
  70. package/fixtures/unclosed-pack-block.md +8 -0
  71. package/package.json +63 -0
  72. package/packs/language/go/rules.md +24 -0
  73. package/packs/language/mdx/rules.md +7 -0
  74. package/packs/language/python/rules.md +8 -0
  75. package/packs/language/rust/rules.md +38 -0
  76. package/packs/language/shell/rules.md +8 -0
  77. package/registry.json +405 -0
  78. package/schemas/evidence.schema.json +43 -0
  79. package/schemas/profile.schema.json +12 -0
  80. package/schemas/registry.schema.json +12 -0
  81. package/skills/orly-babysit-prs/SKILL.md +326 -0
  82. package/skills/orly-spec-new/SKILL.md +157 -0
  83. package/skills/orly-write-integration-test/SKILL.md +470 -0
  84. package/skills/orly-write-unit-test/SKILL.md +520 -0
  85. package/src/cli.test.ts +70 -0
  86. package/src/cli.ts +287 -0
  87. package/src/config.test.ts +226 -0
  88. package/src/config.ts +288 -0
  89. package/src/criteria.test.ts +137 -0
  90. package/src/criteria.ts +166 -0
  91. package/src/criteria_spec.ts +127 -0
  92. package/src/criteria_support.ts +53 -0
  93. package/src/gates.test.ts +183 -0
  94. package/src/gates.ts +169 -0
  95. package/src/gates_spec.test.ts +98 -0
  96. package/src/gates_test_support.ts +194 -0
  97. package/src/git_env.ts +20 -0
  98. package/src/install.test.ts +514 -0
  99. package/src/install.ts +447 -0
  100. package/src/install_nested.test.ts +32 -0
  101. package/src/model.test.ts +31 -0
  102. package/src/model.ts +245 -0
  103. package/src/references.test.ts +47 -0
  104. package/src/references.ts +147 -0
  105. package/src/render.test.ts +60 -0
  106. package/src/render.ts +94 -0
  107. package/src/surfaces.test.ts +43 -0
  108. package/src/surfaces.ts +89 -0
  109. package/src/validation.test.ts +11 -0
  110. package/src/validation.ts +87 -0
  111. package/src/verify.test.ts +13 -0
  112. package/src/verify.ts +59 -0
@@ -0,0 +1,258 @@
1
+ #!/usr/bin/env bash
2
+ # spec-template.sh — enforce docs/TEMPLATE.md across every spec under
3
+ # docs/v*/{pending,active,done}/. Two check families:
4
+ #
5
+ # 1. PROHIBITED patterns (negative space) — TEMPLATE.md "Prohibited" section:
6
+ # time/effort estimates, effort/complexity columns, %-complete, owners, dates.
7
+ # Always BLOCK.
8
+ # 2. REQUIRED-PRESENT + NO-PLACEHOLDER (positive space) — the determinism
9
+ # sections the agent-facing template mandates (PR Intent, Applicable Gates,
10
+ # Prior-Art, Metrics, Decomposition, tiered Test Spec, Product Clarity,
11
+ # Discovery, …) must EXIST and be FILLED: no unfilled {slot} sentinels and
12
+ # no surviving "tpl:" guidance comments (template fill grammar). A spec
13
+ # that omits them forces the executing agent to guess intent. This is the
14
+ # half that makes a spec "built for the agent".
15
+ # 2b. DECLARED-COMMAND PARITY — a pending/active spec's Acceptance Rubric
16
+ # must quote the repository's declared conform + verify.unit commands
17
+ # (.oracle/orly.json) verbatim: the rubric and `orly gate` grade one
18
+ # boundary. Staged/file scope only; done/ specs stay historical.
19
+ #
20
+ # Dispatch façade: dispatch/write_spec.md (SPEC TEMPLATE GATE)
21
+ # Fires in: make lint (after audit-logging, before audit-error-codes).
22
+ #
23
+ # Modes:
24
+ # --staged diff-scope: only specs in `git diff --cached`
25
+ # --all (default) pending+active specs only — current/in-flight work
26
+ # --include-done adds done/ specs to the scan (one-time sweep tool)
27
+ #
28
+ # Family 2 runs ONLY in --staged scope — the spec being authored/edited right
29
+ # now, which is exactly the agent's own output. The bulk scans (--all /
30
+ # --include-done) run Family 1 only, so they behave identically over the whole
31
+ # corpus and never break an existing spec. No legacy carve-out, no heuristics:
32
+ # author a spec → full template required; scan the tree → prohibited-only.
33
+ #
34
+ # Done specs are excluded by default: historical artifacts, not work product.
35
+ # Exits 0 clean, 1 on any blocking finding.
36
+
37
+ set -euo pipefail
38
+
39
+ MODE="${1:-${SCOPE:-all}}"
40
+ ROOT="$(git rev-parse --show-toplevel)"
41
+ cd "$ROOT"
42
+
43
+ FAIL=0
44
+ fail() { printf "FAIL: %s\n" "$*" >&2; FAIL=1; }
45
+ ok() { printf "OK: %s\n" "$*"; }
46
+ note() { printf "NOTE: %s\n" "$*"; }
47
+
48
+ # Discover spec files in scope.
49
+ case "$MODE" in
50
+ --staged|staged)
51
+ mapfile -t SPECS < <(git diff --cached --name-only --diff-filter=ACMRT | grep -E '^docs/v[0-9]+/(pending|active|done)/.*\.md$' || true)
52
+ ;;
53
+ --all|all)
54
+ mapfile -t SPECS < <(find docs/v[0-9]* -type f -name '*.md' 2>/dev/null | grep -E '/(pending|active)/' || true)
55
+ ;;
56
+ --include-done|include-done)
57
+ mapfile -t SPECS < <(find docs/v[0-9]* -type f -name '*.md' 2>/dev/null | grep -E '/(pending|active|done)/' || true)
58
+ ;;
59
+ --file|file)
60
+ # Single named spec, both families — the lifecycle engine's spec.gate
61
+ # criterion calls this so the engine and pre-commit share one enforcer.
62
+ [[ -n "${2:-}" ]] || { printf "usage: %s --file <spec path>\n" "$0" >&2; exit 64; }
63
+ [[ -f "$2" ]] || { printf "FAIL: spec not found: %s\n" "$2" >&2; exit 1; }
64
+ SPECS=("$2")
65
+ ;;
66
+ *)
67
+ printf "usage: %s [--staged|--all|--include-done|--file <path>]\n" "$0" >&2
68
+ exit 64
69
+ ;;
70
+ esac
71
+
72
+ if [[ ${#SPECS[@]} -eq 0 ]]; then
73
+ ok "no spec files in scope ($MODE)"
74
+ exit 0
75
+ fi
76
+
77
+ # ---------------------------------------------------------------------------
78
+ # Family 1 — Prohibited patterns from docs/TEMPLATE.md "Prohibited" section.
79
+ # Each pattern: <severity>:<one-line description>:<regex> — the regex comes
80
+ # LAST because it may contain colons (e.g. the [:|] label-row class); read(1)
81
+ # assigns the remainder of the line to the final field, so the regex survives
82
+ # intact. Descriptions must stay colon-free.
83
+ # Severity: BLOCK = exit 1; INFO = stdout note only. Patterns use ERE (egrep -E)
84
+ # only — no PCRE constructs like (?:...), which grep -E rejects.
85
+ #
86
+ # DESIGN NOTE — patterns are deliberately structural, not prose-level.
87
+ # Bare regex like `\b\d+\s*days?\b` false-positives on legitimate prose
88
+ # ("Day 50", "≥7 days uptime", "within 3 days of install"). Instead we
89
+ # match where time/effort estimates LIVE structurally:
90
+ # - Section headings: `## Estimated effort`, `## Effort`, `## Sizing`
91
+ # - Label rows: `**Effort:** medium`, `**Owner:** alice`
92
+ # - Tilde-bulleted: `- ~2 h survey`, `* ~5 days OAuth setup`
93
+ # (the `~` prefix is Indy's idiom for effort estimates;
94
+ # legitimate prose doesn't use it)
95
+ # This catches the actual drift class without churning real content.
96
+ # ---------------------------------------------------------------------------
97
+ PATTERNS=(
98
+ 'BLOCK:Estimated effort section heading:^#+ .*Estimated [Ee]ffort'
99
+ 'BLOCK:Effort/Complexity/Sizing section heading:^#+ .*\b(Effort|Complexity|Sizing|Cost)\b\s*$'
100
+ 'BLOCK:Scale estimate section heading:^#+ .*\bScale\b.*estimate'
101
+ 'BLOCK:Tilde-bulleted hour estimate (e.g. "- ~2 h"):^[-*]\s*~\s*[0-9]+\s*h\b'
102
+ 'BLOCK:Tilde-bulleted time estimate:^[-*]\s*~\s*[0-9]+\s*(hours?|days?|min(utes?)?)\b'
103
+ 'BLOCK:Effort/Estimate label row:^\s*\*\*\s*(Effort|Estimate|Sizing|Complexity|Cost)\s*(:\s*\*\*|\*\*\s*[:|])'
104
+ 'BLOCK:Owner label row:^\s*\*\*\s*Owner\s*(:\s*\*\*|\*\*\s*[:|])'
105
+ 'BLOCK:Assigned-to label row:^\s*\*\*\s*Assigned to\s*(:\s*\*\*|\*\*\s*[:|])'
106
+ 'BLOCK:Date-bound deadline label:^\s*\*\*\s*(Due|Deadline)\s*(:\s*\*\*|\*\*\s*[:|])\s*[0-9]'
107
+ 'BLOCK:Percentage-complete metric:^[-*\|]?\s*\*?\*?\s*[0-9]+\s*%\s*complete'
108
+ 'BLOCK:Hour-range estimate in effort context:\b(effort|estimate|takes?|spend)\b[^.]{0,40}\b[0-9]+\s*[-–]\s*[0-9]+\s*h\b'
109
+ 'BLOCK:Effort/complexity rating:\b(low|medium|high|small|large)\s*[/|-]\s*(effort|complexity)\b'
110
+ )
111
+
112
+ # Carve-outs — lines that look prohibited but are legitimate. Skip any match.
113
+ SAFE_LINES_RE='^\*\*Date:\*\*|^\s*```|<!--|^\s*//|"[0-9]+\s*(h|hour|day|min)"'
114
+
115
+ # ---------------------------------------------------------------------------
116
+ # Family 2 — Required determinism sections (agent-facing docs/TEMPLATE.md).
117
+ # Each entry: <heading ERE>:<human description>
118
+ # ---------------------------------------------------------------------------
119
+ REQUIRED_SECTIONS=(
120
+ '^#+ .*PR Intent:PR Intent & comprehension handshake'
121
+ '^#+ .*Applicable Rules:Applicable Rules'
122
+ '^#+ .*Applicable Gates:Applicable Gates'
123
+ '^#+ .*Overview:Overview'
124
+ '^#+ .*(Prior-Art|Reference Implementation):Prior-Art / Reference Implementations'
125
+ '^#+ .*Files Changed:Files Changed (blast radius)'
126
+ '^#+ .*(Decomposition|Alternatives):Decomposition & alternatives (patch vs refactor)'
127
+ '^#+ .*Sections:Sections (implementation slices)'
128
+ '^#+ .*Metrics.*Observability:Metrics & Observability'
129
+ '^#+ .*Interfaces:Interfaces'
130
+ '^#+ .*Failure Modes:Failure Modes'
131
+ '^#+ .*Invariants:Invariants'
132
+ '^#+ .*Test Specification:Test Specification'
133
+ '^#+ .*Acceptance (Criteria|Rubric):Acceptance Rubric (legacy heading Acceptance Criteria accepted)'
134
+ '^#+ .*Product Clarity:Product Clarity (authoring record)'
135
+ '^#+ .*Discovery:Discovery (consult log)'
136
+ )
137
+
138
+ # Template residue — strings that exist ONLY in the unfilled template. Their
139
+ # survival in a pending/active spec means the section was never filled. The
140
+ # "<!-- tpl:" marker is the template's fill grammar: every guidance comment is
141
+ # deleted at authoring, so any survivor is residue by construction.
142
+ PLACEHOLDER_SENTINELS=(
143
+ '<!-- tpl:'
144
+ 'Title — testable, not vague'
145
+ 'one-line reason}'
146
+ 'Slice title}'
147
+ 'path/to/file.ext'
148
+ 'test_<short_name>'
149
+ 'why this is the right pattern to mirror'
150
+ '{outcome the user can observe}'
151
+ '{exit 0 / substring / 0 matches}'
152
+ 'path/to/spec_or_doc.md'
153
+ '{path/to/old_file.ext}'
154
+ '{old_symbol}'
155
+ '{one sentence that could be a test name}'
156
+ '{smallest verifiable behaviour}'
157
+ '{one-line behavioural claim}'
158
+ '{conform command from .oracle/orly.json'
159
+ '{verify.unit command from .oracle/orly.json'
160
+ "{gate from this repository's AGENTS.md dispatch index}"
161
+ )
162
+
163
+ # ---------------------------------------------------------------------------
164
+ # Family 2b — declared-command parity (TEMPLATE.md "Command source rule").
165
+ # A pending/active spec's Acceptance Rubric must quote the repository's
166
+ # declared conform + verify.unit commands VERBATIM — the same set `orly gate`
167
+ # runs — so the spec's ship gate and the mechanical PR gate grade one
168
+ # boundary. Reads .oracle/orly.json with bun (orly's own runtime). Done specs
169
+ # are historical records and are never retrofitted. No config, no bun, or no
170
+ # declared commands → NOTE and skip; this check never guesses.
171
+ # ---------------------------------------------------------------------------
172
+ scan_commands() {
173
+ local spec="$1" cfg=".oracle/orly.json" cmds
174
+ case "$spec" in
175
+ */pending/*|*/active/*) ;;
176
+ *) return 0 ;;
177
+ esac
178
+ [[ -f "$cfg" ]] || { note "$spec — no $cfg; declared-command parity skipped"; return 0; }
179
+ command -v bun >/dev/null 2>&1 || { note "$spec — bun unavailable; declared-command parity skipped"; return 0; }
180
+ cmds="$(bun -e '
181
+ const cfg = await Bun.file(".oracle/orly.json").json().catch(() => null);
182
+ const commands = cfg && typeof cfg.commands === "object" && cfg.commands !== null ? cfg.commands : {};
183
+ for (const key of ["conform", "verify.unit"]) {
184
+ for (const argv of Array.isArray(commands[key]) ? commands[key] : []) {
185
+ if (Array.isArray(argv) && argv.length > 0) console.log(argv.join(" "));
186
+ }
187
+ }' 2>/dev/null || true)"
188
+ [[ -n "$cmds" ]] || { note "$spec — no declared conform/verify.unit commands; parity skipped"; return 0; }
189
+ local miss=0 cmd
190
+ while IFS= read -r cmd; do
191
+ [[ -z "$cmd" ]] && continue
192
+ grep -qF "$cmd" "$spec" && continue
193
+ fail "$spec — rubric misses declared command verbatim: \"$cmd\" (.oracle/orly.json — the set orly gate runs)"
194
+ miss=$((miss + 1))
195
+ done <<<"$cmds"
196
+ if [[ $miss -eq 0 ]]; then ok "$spec — rubric quotes the declared conform/verify.unit commands"; fi
197
+ return 0
198
+ }
199
+
200
+ scan_spec() {
201
+ local spec="$1"
202
+ local hits=0
203
+ local pattern desc severity line content
204
+ while IFS=: read -r severity desc pattern; do
205
+ while IFS= read -r match; do
206
+ [[ -z "$match" ]] && continue
207
+ if grep -qE "$SAFE_LINES_RE" <<<"$match"; then
208
+ continue
209
+ fi
210
+ line="${match%%:*}"
211
+ content="${match#*:}"
212
+ content="${content%$'\r'}"
213
+ if [[ "$severity" = "BLOCK" ]]; then
214
+ fail "$spec:$line — $desc"
215
+ printf " > %s\n" "$content" >&2
216
+ else
217
+ note "$spec:$line — $desc"
218
+ fi
219
+ hits=$((hits + 1))
220
+ done < <(grep -nE "$pattern" "$spec" 2>/dev/null || true)
221
+ done < <(printf '%s\n' "${PATTERNS[@]}")
222
+ if [[ $hits -eq 0 ]]; then ok "$spec — no prohibited patterns"; fi
223
+ return 0
224
+ }
225
+
226
+ # Family 2 — required determinism sections present + no template residue.
227
+ # BLOCK only; called solely in --staged scope (a spec being authored now).
228
+ scan_required() {
229
+ local spec="$1"
230
+ local miss=0 entry sec desc sentinel
231
+ for entry in "${REQUIRED_SECTIONS[@]}"; do
232
+ sec="${entry%%:*}"; desc="${entry#*:}"
233
+ grep -qE "$sec" "$spec" && continue
234
+ fail "$spec — missing required section: $desc"
235
+ miss=$((miss + 1))
236
+ done
237
+ for sentinel in "${PLACEHOLDER_SENTINELS[@]}"; do
238
+ grep -qF "$sentinel" "$spec" || continue
239
+ fail "$spec — unfilled template placeholder: \"$sentinel\""
240
+ miss=$((miss + 1))
241
+ done
242
+ if [[ $miss -eq 0 ]]; then ok "$spec — required sections present, no placeholders"; fi
243
+ return 0
244
+ }
245
+
246
+ for spec in "${SPECS[@]}"; do
247
+ [[ -f "$spec" ]] || continue
248
+ scan_spec "$spec"
249
+ case "$MODE" in --staged|staged|--file|file) scan_required "$spec"; scan_commands "$spec" ;; esac
250
+ done
251
+
252
+ if [[ $FAIL -ne 0 ]]; then
253
+ printf "\n🔴 SPEC TEMPLATE GATE: violations found. See dispatch/write_spec.md.\n" >&2
254
+ exit 1
255
+ fi
256
+
257
+ ok "SPEC TEMPLATE GATE: clean (${#SPECS[@]} specs)"
258
+ exit 0
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env bash
2
+ # sql-mod.sh — SQL statements must live in domain sql.zig, not inline in Zig source.
3
+ #
4
+ # Dispatch façade: dispatch/write_zig.md (SQLMOD). Extracted from write_zig.sh's
5
+ # former inline `zig_sql_module_gate` so the Zig dispatch DELEGATES to a leaf
6
+ # rather than doing the work itself (the dispatch/lib.sh contract: "It never does
7
+ # the work itself — the leaf helpers in audits/ do"). This is what lets
8
+ # evals/dispatch/coverage.sh see SQLMOD as a wired dispatch_run_helper row.
9
+ #
10
+ # Rule: a *.zig file (excluding sql.zig, *_test.zig, test_fixtures_*.zig) must
11
+ # not DEFINE SQL — neither
12
+ # - a `const <SQL-ish name> = …` (SELECT_/INSERT_/…/_SQL/_QUERY), nor
13
+ # - a Zig multiline-string body opening with `\\WITH|SELECT|INSERT|…`.
14
+ # New files are whole-file scanned; existing files are scanned only for newly
15
+ # ADDED lines (staged diff), so pre-existing inline SQL is not retroactively
16
+ # flagged — only new SQL must land in a domain sql.zig.
17
+ #
18
+ # Modes:
19
+ # --staged diff-scope: staged *.zig (new files whole-scanned, existing diff-scanned)
20
+ # --all (default) every src/*.zig whole-scanned
21
+ #
22
+ # Exits 0 clean, 1 on any inline-SQL hit.
23
+
24
+ set -euo pipefail
25
+
26
+ MODE="${1:-${SCOPE:---all}}"
27
+ ROOT="$(git rev-parse --show-toplevel)"
28
+ cd "$ROOT"
29
+
30
+ FAIL=0
31
+ fail() { printf "FAIL: %s\n" "$*" >&2; FAIL=1; }
32
+ ok() { printf "OK: %s\n" "$*"; }
33
+
34
+ # Kept byte-identical to the former write_zig.sh regexes so behaviour is
35
+ # unchanged by the extraction.
36
+ DEF_RE='^[[:space:]]*(pub[[:space:]]+)?const[[:space:]]+(SELECT|INSERT|UPDATE|DELETE|UPSERT|LIST|COUNT|EXISTS|[A-Z0-9_]+_(SQL|QUERY)|(SELECT|INSERT|UPDATE|DELETE|UPSERT|LIST|COUNT|EXISTS)_[A-Z0-9_]+)[[:space:]]*='
37
+ BODY_RE='^[[:space:]]*\\\\(WITH|SELECT|INSERT|UPDATE|DELETE|CREATE|ALTER|DROP)[[:space:]]'
38
+
39
+ definition_hit() { grep -nE "$DEF_RE" "$1" | head -1 || true; }
40
+ body_hit() { grep -nE "$BODY_RE" "$1" | head -1 || true; }
41
+ added_hit() {
42
+ git diff --cached -U0 -- "$1" \
43
+ | awk '/^\+[^+]/ { sub(/^\+/, ""); print }' \
44
+ | grep -nE "$DEF_RE|$BODY_RE" | head -1 || true
45
+ }
46
+
47
+ case "$MODE" in
48
+ --staged|staged)
49
+ mapfile -t FILES < <(git diff --cached --name-only --diff-filter=ACMRT -- '*.zig' || true)
50
+ STAGED=1 ;;
51
+ --all|all)
52
+ mapfile -t FILES < <(find src -type f -name '*.zig' 2>/dev/null || true)
53
+ STAGED=0 ;;
54
+ *)
55
+ printf "usage: %s [--staged|--all]\n" "$0" >&2
56
+ exit 64 ;;
57
+ esac
58
+
59
+ if [[ ${#FILES[@]} -eq 0 ]]; then
60
+ ok "no zig source files in scope ($MODE)"
61
+ exit 0
62
+ fi
63
+
64
+ scanned=0
65
+ for f in "${FILES[@]}"; do
66
+ [[ -n "$f" ]] || continue
67
+ case "$(basename "$f")" in
68
+ sql.zig|*_test.zig|test_fixtures_*.zig) continue ;;
69
+ esac
70
+ [[ -f "$f" ]] || continue
71
+ scanned=$((scanned + 1))
72
+
73
+ is_new=0
74
+ if [[ "$STAGED" -eq 1 ]]; then
75
+ status="$(git diff --cached --name-status -- "$f" | awk 'NR==1 {print $1}')"
76
+ if [[ "$status" == A* ]]; then
77
+ is_new=1
78
+ elif ! git ls-files --error-unmatch "$f" >/dev/null 2>&1; then
79
+ is_new=1
80
+ fi
81
+ else
82
+ is_new=1 # --all: no diff context — whole-file scan
83
+ fi
84
+
85
+ if [[ "$is_new" -eq 1 ]]; then
86
+ line="$(definition_hit "$f")"
87
+ [[ -n "$line" ]] || line="$(body_hit "$f")"
88
+ else
89
+ line="$(added_hit "$f")"
90
+ fi
91
+
92
+ if [[ -n "$line" ]]; then
93
+ fail "$f adds inline SQL ($line); move statements to domain sql.zig"
94
+ fi
95
+ done
96
+
97
+ if [[ "$FAIL" -ne 0 ]]; then
98
+ printf "\n🔴 SQLMOD: inline SQL found — move statements to a domain sql.zig. See dispatch/write_zig.md.\n" >&2
99
+ exit 1
100
+ fi
101
+ ok "SQLMOD: clean ($scanned file(s) scanned, no new inline SQL)"
102
+ exit 0
package/audits/ufs.sh ADDED
@@ -0,0 +1,303 @@
1
+ #!/usr/bin/env bash
2
+ # ufs.sh — enforce RULE UFS (Unified Form for Symbols) across the worktree.
3
+ #
4
+ # Dispatch façade: dispatch/write_any.md (UFS Gate)
5
+ # Fires in: make lint, CONFORM.
6
+ #
7
+ # Generic detection — no manifest of known literals, so the audit scales
8
+ # as the codebase grows. Three classes of violation:
9
+ #
10
+ # 1. string-dup-file — same string literal ≥2× in one source file
11
+ # 2. numeric-suspect — power-of-ten or unit-factor numeric not bound
12
+ # to a const, not on a pin-test carve-out line
13
+ # 3. cross-runtime-orphan — SCREAMING_SNAKE const defined in one runtime
14
+ # but missing from a sibling runtime the diff touches
15
+ #
16
+ # Carve-out: any `// pin test: literal is the contract` comment on or
17
+ # above the offending line excludes that line from numeric-suspect.
18
+ # A `const` / `pub const` / `export const` declaration line is likewise
19
+ # exempt for both per-file checks (is_const_decl + its string-dup mirror
20
+ # below) — binding the literal to a name on a const line clears the hit.
21
+ #
22
+ # Staged-scope semantics: --staged narrows WHICH FILES are scanned, not
23
+ # how much of each — a staged file is audited in FULL, so staging a file
24
+ # for an unrelated one-line change drags its pre-existing literal debt
25
+ # into the commit. Broad sweeps (renames touching N files) surface N
26
+ # files' latent hits at once; plan that cleanup before staging.
27
+ #
28
+ # Scope (M70):
29
+ # Walks the full working tree via `git ls-files` — sees staged content
30
+ # because the index is what `ls-files` reports. Pre-commit-safe: a fix
31
+ # staged but not yet committed satisfies the check on the same hook run.
32
+ # Deterministic negative fixtures under `evals/dispatch/fixtures/` are
33
+ # excluded from storage scans. The dispatch evaluator copies each fixture
34
+ # into `src/` before asserting its expected pass or failure result.
35
+ # The previous `--diff` (BASE...HEAD) mode was retired with M70 because
36
+ # it was blind to the index at pre-commit time.
37
+ #
38
+ # Usage:
39
+ # ufs.sh # full-codebase scan (default)
40
+ # ufs.sh --all # alias for default
41
+ # ufs.sh --staged # narrow per-file checks (string-dup, numeric) to
42
+ # # `git diff --cached`; cross-runtime parity stays full-tree
43
+ #
44
+ # Exits 0 clean, 1 on any blocking violation.
45
+
46
+ set -euo pipefail
47
+
48
+ # Default: full-codebase scan (`--all` is an explicit alias). `--staged` is the
49
+ # pre-commit lens — it narrows the per-file checks to `git diff --cached`. The
50
+ # retired `--diff` (BASE...HEAD) mode stays rejected; `--staged` reads the index
51
+ # and so is not blind to staged-but-uncommitted fixes (M70's concern).
52
+ MODE="${1:-}"
53
+ case "$MODE" in
54
+ ""|--all|all) MODE="--all" ;;
55
+ --staged|staged) MODE="--staged" ;;
56
+ *)
57
+ printf "usage: %s [--all|--staged]\n" "$0" >&2
58
+ printf "note: --diff was retired in M70 — see dispatch/write_any.md (UFS Gate → Scope). Use --staged for the pre-commit (index) lens.\n" >&2
59
+ exit 2
60
+ ;;
61
+ esac
62
+ ROOT="$(git rev-parse --show-toplevel)"
63
+ cd "$ROOT"
64
+
65
+ FAIL=0
66
+ violations=()
67
+ record() { violations+=("$*"); FAIL=1; }
68
+ ok() { printf "OK: %s\n" "$*"; }
69
+
70
+ # ── File scope ──────────────────────────────────────────────────────────────
71
+
72
+ is_source() {
73
+ local f="$1"
74
+ case "$f" in
75
+ vendor/*|third_party/*|.zig-cache/*|*/node_modules/*|evals/dispatch/fixtures/*|*.tsbuildinfo) return 1 ;;
76
+ *_test.zig|*.test.ts|*.test.tsx|*.test.js|*.test.jsx|*.unit.test.js|*.spec.ts) ;; # tests in scope
77
+ esac
78
+ case "$f" in
79
+ *.zig|*.ts|*.tsx|*.js|*.jsx) return 0 ;;
80
+ *) return 1 ;;
81
+ esac
82
+ }
83
+
84
+ # Per-file check scope (string-dup-file, numeric-suspect). --staged narrows to
85
+ # the commit; cross-runtime-orphan (below) always scans the full tree.
86
+ if [ "$MODE" = "--staged" ]; then
87
+ scope_files() { git diff --cached --name-only --diff-filter=ACMRT; }
88
+ else
89
+ scope_files() { git ls-files; }
90
+ fi
91
+ mapfile -t FILES < <(scope_files | while read -r f; do
92
+ is_source "$f" && echo "$f"
93
+ done)
94
+
95
+ [ "${#FILES[@]}" -eq 0 ] && { ok "audit-ufs: no source files in scope"; exit 0; }
96
+
97
+ # ── 1. string-dup-file ──────────────────────────────────────────────────────
98
+ # Extract double-quoted string literals (best-effort regex; ignores escapes
99
+ # inside strings — fine for this discipline check, not a parser). Skip
100
+ # 1-char strings, common short labels, doc-strings, JSON keys.
101
+
102
+ # Perf (M70): single awk over all files instead of per-file pipeline
103
+ # (was 5 forks × ~760 files = ~20s). Group counts by FILENAME so the
104
+ # "≥2 occurrences in one file" semantic is preserved.
105
+ #
106
+ # Subshell-propagation fix (M70): the loop reads from a process
107
+ # substitution (not a pipe) so `record` mutates FAIL / violations in
108
+ # the parent shell. The previous pipe-into-while form ran the loop in
109
+ # a subshell and silently dropped violations.
110
+ while IFS= read -r row; do
111
+ record "$row"
112
+ done < <(awk '
113
+ FNR == 1 {
114
+ prev_file = FILENAME
115
+ # Test files: repetition is fixture data, not magic strings.
116
+ # Skip string-dup-file for tests; other checks still apply.
117
+ is_test = (FILENAME ~ /(_test\.zig|\.test\.|\.spec\.|\.unit\.test|\.integration\.test|\/test\/|\/tests\/)/)
118
+ }
119
+ is_test { next }
120
+ # ui/ files: extracting class-strings or short literals to file-local
121
+ # consts is fragile in TypeScript (type-position uses widen string|
122
+ # literal types, and the cleanup belongs in a UI-aware spec). Skip
123
+ # string-dup-file for ui/packages/*/src; cross-runtime-orphan still
124
+ # runs against ui/ to catch ERR_* parity drift.
125
+ # macOS/BSD awk aborts on a `/` inside a `[...]` class in a regex *literal*
126
+ # ("nonterminated character class"); use a dynamic-regex STRING — identical
127
+ # match on gawk, portable on BWK awk (the macOS default). Do not re-inline.
128
+ FILENAME ~ "^ui/packages/[^/]+/(src|app|tests|components|lib|hooks)/" { next }
129
+ FNR == 1 { in_test_block = 0; test_depth = 0; in_block_comment = 0 }
130
+ {
131
+ line = $0
132
+ # Block-comment exclusion (TS/JS/Zig non-applicable): skip lines inside /* ... */
133
+ if (in_block_comment) {
134
+ if (line ~ /\*\//) in_block_comment = 0
135
+ next
136
+ }
137
+ if (line ~ /\/\*/ && line !~ /\*\//) {
138
+ in_block_comment = 1
139
+ next
140
+ }
141
+ # Zig multi-line string literal — lines start with `\\` after whitespace.
142
+ if (FILENAME ~ /\.zig$/ && line ~ /^[[:space:]]*\\\\/) next
143
+ # Inline-test exclusion (Zig): track depth across `test "..." {` blocks.
144
+ if (FILENAME ~ /\.zig$/) {
145
+ if (in_test_block) {
146
+ test_depth += gsub(/\{/, "{", line) - gsub(/\}/, "}", line)
147
+ if (test_depth <= 0) in_test_block = 0
148
+ line = $0 # restore
149
+ next
150
+ }
151
+ if (line ~ /^test[[:space:]]+"/) {
152
+ in_test_block = 1
153
+ tmp = line
154
+ test_depth = gsub(/\{/, "{", tmp) - gsub(/\}/, "}", tmp)
155
+ if (test_depth <= 0) in_test_block = 0
156
+ next
157
+ }
158
+ }
159
+ sub(/\/\/.*$/, "", line)
160
+ # Strip single-line /* ... */ inline block comments (jsdoc/etc) so
161
+ # literals inside them are not counted.
162
+ gsub(/\/\*[^*]*\*+([^\/*][^*]*\*+)*\//, "", line)
163
+ # Const-BINDING carve-out (narrower than the numeric-suspect is_const_decl
164
+ # exemption below — deliberately): a literal that IS the right-hand side of a
165
+ # `const`/`pub const`/`export const` binding is its single-source DEFINITION,
166
+ # not a magic-string use. Two distinct named constants may legitimately share
167
+ # a value across domains (e.g. a runner status and a lease status both
168
+ # "active") — RULE UFS targets UN-named repetition, and both sites there are
169
+ # already named. But a literal passed as a CALL ARGUMENT on a const line
170
+ # (`const x = foo("lit");`) is NOT named by that const — it counts and flags
171
+ # like any other use. (A prior line-level skip exempted those too and silently
172
+ # gutted the check for most Zig code; the ufs_dup_string eval fixture pins the
173
+ # binding-level semantic.)
174
+ if (line ~ /(^|[[:space:]])(pub[[:space:]]+const|export[[:space:]]+const|const)[[:space:]]+[A-Za-z_$][A-Za-z0-9_$]*([[:space:]]*:[^=]*)?[[:space:]]*=[[:space:]]*"((\\.)|[^\\"])+"[[:space:]]*(as[[:space:]]+const)?[[:space:]]*[;,]?[[:space:]]*$/) next
175
+ rest = line
176
+ # Strip Zig identifier-escape syntax @"name" — body is an identifier,
177
+ # not a string literal, but the regex below would otherwise match it.
178
+ gsub(/@"[^"]*"/, "", rest)
179
+ # Strip empty string literals so the regex below cannot fuse
180
+ # two adjacent Zig literals through their inner gap.
181
+ gsub(/""/, "", rest)
182
+ # Match a quoted string literal honouring \" escapes: open quote,
183
+ # then runs of (backslash+any-char) | (non-backslash-non-quote),
184
+ # then close quote. Min 3 chars total ensures literal body ≥1 char
185
+ # (back-compat with the {2,} body-min from the prior regex).
186
+ while (match(rest, /"((\\.)|[^\\"])+"/)) {
187
+ lit = substr(rest, RSTART, RLENGTH)
188
+ rest = substr(rest, RSTART + RLENGTH)
189
+ if (length(lit) < 4) continue
190
+ if (lit ~ /^"(http|https|file|\/|\\\\|\\\\n)/) continue
191
+ key = FILENAME "\034" lit
192
+ count[key]++
193
+ file_of[key] = FILENAME
194
+ lit_of[key] = lit
195
+ }
196
+ }
197
+ END {
198
+ for (k in count) {
199
+ if (count[k] >= 2) {
200
+ printf "string-dup-file %s %s %d\n", file_of[k], lit_of[k], count[k]
201
+ }
202
+ }
203
+ }
204
+ ' "${FILES[@]}")
205
+
206
+ # ── 2. numeric-suspect ──────────────────────────────────────────────────────
207
+ # Flag bare power-of-ten and unit-factor numerics in expressions.
208
+ # Pattern matches: 1_000, 1_000_000, 1_000_000_000, 1e3, 1e6, 1e9,
209
+ # 60, 3600, 86400, 1024, 1048576, 10_000_000, 100_000.
210
+ # A line is a violation if:
211
+ # - it contains one of those patterns
212
+ # - the line is NOT a const declaration (`pub const`, `export const`, `const`)
213
+ # - the line is NOT marked `// pin test: literal is the contract`
214
+ # - the line above is NOT marked `// pin test: literal is the contract`
215
+
216
+ # Boundaries are capturing groups, not \b: macOS/BSD awk treats \b as a backspace,
217
+ # so word-boundary anchors silently match nothing. The leading/trailing boundary
218
+ # chars are trimmed back off the extracted token below (see RSTART/RLENGTH use).
219
+ NUMERIC_RE='(^|[^0-9A-Za-z_])(1[_e]?0{3,12}|1_000(_000)*|10_000_000|100_000|1024|1048576|3600|86400)([^0-9A-Za-z_]|$)'
220
+
221
+ # Perf (M70): single awk across all files; FNR == 1 detects file boundary
222
+ # so the "prev line carve-out" stays per-file. Process substitution keeps
223
+ # the while loop in the parent shell so record() mutates FAIL.
224
+ while IFS= read -r row; do
225
+ record "$row"
226
+ done < <(awk -v re="$NUMERIC_RE" '
227
+ FNR == 1 { prev = "" }
228
+ {
229
+ line = $0
230
+ is_pin_now = (line ~ /pin test: literal is the contract/)
231
+ is_pin_above = (prev ~ /pin test: literal is the contract/)
232
+ is_const_decl = (line ~ /(^|[[:space:]])(pub[[:space:]]+const|export[[:space:]]+const|const)[[:space:]]/)
233
+ stripped = line
234
+ sub(/\/\/.*$/, "", stripped)
235
+ sub(/#.*$/, "", stripped)
236
+ if (!is_pin_now && !is_pin_above && !is_const_decl && match(stripped, re)) {
237
+ tok = substr(stripped, RSTART, RLENGTH)
238
+ gsub(/^[^0-9]+|[^0-9_e]+$/, "", tok) # trim the captured boundary chars
239
+ printf "numeric-suspect %s:%d %s\n", FILENAME, FNR, tok
240
+ }
241
+ prev = line
242
+ }
243
+ ' "${FILES[@]}")
244
+
245
+ # ── 3. cross-runtime-orphan ─────────────────────────────────────────────────
246
+ # Full-codebase ERR_* parity check. Scoped to ERR_* prefix because that's
247
+ # the cross-runtime contract surface (server error codes consumed by
248
+ # clients). Server (Zig) is the source of truth — every JS/TS ERR_* must
249
+ # have a matching Zig pub const ERR_*. Zig-only ERR_* consts are fine
250
+ # (server-internal codes don't need a client mirror).
251
+ #
252
+ # Scans the *working tree* via `git ls-files` — sees staged content even
253
+ # before it lands in HEAD, closing the previous diff-mode blindspot where
254
+ # a fix staged in pre-commit couldn't satisfy a check that only read
255
+ # committed history.
256
+ #
257
+ # Perf (M70): batched `xargs grep` (single process across all files)
258
+ # instead of `xargs -I{} grep` (one process per file). On agentsfleet the
259
+ # server-side scan dropped from ~30s to <2s.
260
+
261
+ zig_err=$(git ls-files -z -- 'src/*.zig' 2>/dev/null \
262
+ | { grep -zvE '_test\.zig$|^src/zbench_fixtures\.zig$' || true; } \
263
+ | xargs -0 grep -hE '^pub const ERR_[A-Z][A-Z0-9_]+[[:space:]]*=' 2>/dev/null \
264
+ | grep -oE 'ERR_[A-Z][A-Z0-9_]+' | sort -u || true)
265
+
266
+ js_err=$(git ls-files -z -- 'agentsfleet/src/*.js' 'agentsfleet/src/*.jsx' 'agentsfleet/src/*.ts' 'agentsfleet/src/*.tsx' 2>/dev/null \
267
+ | { grep -zvE '\.test\.|\.spec\.' || true; } \
268
+ | xargs -0 grep -hE '^export const ERR_[A-Z][A-Z0-9_]+[[:space:]]*=' 2>/dev/null \
269
+ | grep -oE 'ERR_[A-Z][A-Z0-9_]+' | sort -u || true)
270
+
271
+ ui_err=$(git ls-files -z -- 'ui/packages/*/src/*.ts' 'ui/packages/*/src/*.tsx' 2>/dev/null \
272
+ | { grep -zvE '\.test\.|\.spec\.' || true; } \
273
+ | xargs -0 grep -hE '^export const ERR_[A-Z][A-Z0-9_]+[[:space:]]*=' 2>/dev/null \
274
+ | grep -oE 'ERR_[A-Z][A-Z0-9_]+' | sort -u || true)
275
+
276
+ # Every JS ERR_* must exist in Zig.
277
+ for c in $js_err; do
278
+ if ! echo "$zig_err" | grep -qx "$c"; then
279
+ record "cross-runtime-orphan $c absent-in-zig"
280
+ fi
281
+ done
282
+ # Every TS (UI) ERR_* must exist in Zig.
283
+ for c in $ui_err; do
284
+ if ! echo "$zig_err" | grep -qx "$c"; then
285
+ record "cross-runtime-orphan $c absent-in-zig"
286
+ fi
287
+ done
288
+
289
+ # ── Report ──────────────────────────────────────────────────────────────────
290
+
291
+ if [ "$FAIL" -eq 0 ]; then
292
+ ok "audit-ufs: no violations across ${#FILES[@]} file(s)"
293
+ exit 0
294
+ fi
295
+
296
+ printf "🔴 UFS GATE — %d violation(s):\n" "${#violations[@]}" >&2
297
+ for v in "${violations[@]}"; do
298
+ printf " %s\n" "$v" >&2
299
+ done
300
+ printf "\nResolve by either (1) extract to a named const + replace all sites,\n" >&2
301
+ printf "(2) add the matching const in the missing sibling runtime same-commit,\n" >&2
302
+ printf "or (3) annotate '// pin test: literal is the contract' on/above the line.\n" >&2
303
+ exit 1