@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.
- package/.githooks/pre-commit +42 -0
- package/.githooks/pre-push +67 -0
- package/AGENTS.md +348 -0
- package/LICENSE +21 -0
- package/README.md +249 -0
- package/SOUL.md +90 -0
- package/audits/agents-md.md +497 -0
- package/audits/agents-md.sh +353 -0
- package/audits/cross-tier-rates.sh +105 -0
- package/audits/data.sh +185 -0
- package/audits/deinit-pairs.sh +224 -0
- package/audits/design-tokens.sh +143 -0
- package/audits/doc-read.sh +183 -0
- package/audits/error-codes.sh +221 -0
- package/audits/fixtures/clean.diff +15 -0
- package/audits/fixtures/dirty.diff +16 -0
- package/audits/lifecycle-anchors.sh +22 -0
- package/audits/logging.sh +217 -0
- package/audits/msid-ui.sh +144 -0
- package/audits/parity-dispatch.sh +51 -0
- package/audits/rule-ledger-lib.sh +264 -0
- package/audits/rule-ledger.sh +225 -0
- package/audits/rule-paths.sh +108 -0
- package/audits/spec-template.sh +258 -0
- package/audits/sql-mod.sh +102 -0
- package/audits/ufs.sh +303 -0
- package/bin/orly +26 -0
- package/core/operating-model.md +256 -0
- package/dispatch/edit_rules.md +69 -0
- package/dispatch/lib.sh +213 -0
- package/dispatch/lifecycle.md +187 -0
- package/dispatch/name_architecture.md +93 -0
- package/dispatch/verify.md +111 -0
- package/dispatch/write_any.md +740 -0
- package/dispatch/write_any.sh +49 -0
- package/dispatch/write_auth.md +36 -0
- package/dispatch/write_changelog.md +64 -0
- package/dispatch/write_documentation.md +45 -0
- package/dispatch/write_go.md +24 -0
- package/dispatch/write_http.md +45 -0
- package/dispatch/write_mdx.md +7 -0
- package/dispatch/write_python.md +8 -0
- package/dispatch/write_rust.md +38 -0
- package/dispatch/write_shell.md +8 -0
- package/dispatch/write_spec.md +105 -0
- package/dispatch/write_sql.md +112 -0
- package/dispatch/write_sql.sh +36 -0
- package/dispatch/write_ts_adhere_bun.md +493 -0
- package/dispatch/write_ts_adhere_bun.sh +37 -0
- package/dispatch/write_zig.md +852 -0
- package/dispatch/write_zig.sh +40 -0
- package/docs/CHANGELOG_VOICE.md +18 -0
- package/docs/DISPATCH_ARCHITECTURE.md +664 -0
- package/docs/DISPATCH_REVIEW_DISPOSITION.md +65 -0
- package/docs/DOCUMENTATION_RULES.md +209 -0
- package/docs/EXECUTE_DOC_READS.md +29 -0
- package/docs/HARNESS_VERIFY_OUTPUT.md +40 -0
- package/docs/LIFECYCLE_PATTERNS.md +316 -0
- package/docs/LOGGING_STANDARD.md +397 -0
- package/docs/ORLY_ARCHITECTURE.md +129 -0
- package/docs/REST_API_DESIGN_GUIDELINES.md +783 -0
- package/docs/RULE_ENFORCEMENT.md +41 -0
- package/docs/SCHEMA_CONVENTIONS.md +93 -0
- package/docs/TEMPLATE.md +411 -0
- package/docs/VERIFY_TIERS.md +57 -0
- package/docs/greptile-learnings/README.md +51 -0
- package/docs/greptile-learnings/RULES.md +1045 -0
- package/fixtures/registry-invalid.json +1 -0
- package/fixtures/registry-valid.json +18 -0
- package/fixtures/unclosed-pack-block.md +8 -0
- package/package.json +63 -0
- package/packs/language/go/rules.md +24 -0
- package/packs/language/mdx/rules.md +7 -0
- package/packs/language/python/rules.md +8 -0
- package/packs/language/rust/rules.md +38 -0
- package/packs/language/shell/rules.md +8 -0
- package/registry.json +405 -0
- package/schemas/evidence.schema.json +43 -0
- package/schemas/profile.schema.json +12 -0
- package/schemas/registry.schema.json +12 -0
- package/skills/orly-babysit-prs/SKILL.md +326 -0
- package/skills/orly-spec-new/SKILL.md +157 -0
- package/skills/orly-write-integration-test/SKILL.md +470 -0
- package/skills/orly-write-unit-test/SKILL.md +520 -0
- package/src/cli.test.ts +70 -0
- package/src/cli.ts +287 -0
- package/src/config.test.ts +226 -0
- package/src/config.ts +288 -0
- package/src/criteria.test.ts +137 -0
- package/src/criteria.ts +166 -0
- package/src/criteria_spec.ts +127 -0
- package/src/criteria_support.ts +53 -0
- package/src/gates.test.ts +183 -0
- package/src/gates.ts +169 -0
- package/src/gates_spec.test.ts +98 -0
- package/src/gates_test_support.ts +194 -0
- package/src/git_env.ts +20 -0
- package/src/install.test.ts +514 -0
- package/src/install.ts +447 -0
- package/src/install_nested.test.ts +32 -0
- package/src/model.test.ts +31 -0
- package/src/model.ts +245 -0
- package/src/references.test.ts +47 -0
- package/src/references.ts +147 -0
- package/src/render.test.ts +60 -0
- package/src/render.ts +94 -0
- package/src/surfaces.test.ts +43 -0
- package/src/surfaces.ts +89 -0
- package/src/validation.test.ts +11 -0
- package/src/validation.ts +87 -0
- package/src/verify.test.ts +13 -0
- 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
|