@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,224 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# deinit-pairs.sh — verify init/deinit pairing for Zig structs.
|
|
3
|
+
#
|
|
4
|
+
# Dispatch façade: dispatch/write_zig.md (lifecycle / LIFECYCLE GATE)
|
|
5
|
+
# Standalone tool — not wired into make lint or pre-commit (per Indy's
|
|
6
|
+
# direction, gates fire pre-emptively via printed proof-blocks during
|
|
7
|
+
# agent work; lint integration is intentionally absent).
|
|
8
|
+
#
|
|
9
|
+
# Lifecycle methods recognized (per LIFECYCLE_PATTERNS.md §10A.LC2):
|
|
10
|
+
# init / deinit / close / release / destroy / shutdown / dispose / free
|
|
11
|
+
#
|
|
12
|
+
# Refined pairing rule:
|
|
13
|
+
# A `pub fn init` requires a matching cleanup method (deinit / close /
|
|
14
|
+
# release / destroy / shutdown / dispose / free) IN THE SAME FILE only
|
|
15
|
+
# when the init either:
|
|
16
|
+
# (a) allocates inside its body
|
|
17
|
+
# (alloc/create/dupe/dupeZ/initCapacity/allocPrint/realloc, plus
|
|
18
|
+
# std.fs.File.open|create, std.Thread.spawn, etc.), OR
|
|
19
|
+
# (b) returns a heap pointer (`*T` or `!*T` or `*Self` or `!*Self`).
|
|
20
|
+
#
|
|
21
|
+
# Pure value-type init (e.g. `pub fn init() Self` returning a struct
|
|
22
|
+
# literal with no heap state) is permitted without deinit per
|
|
23
|
+
# LIFECYCLE_PATTERNS.md §4 decision matrix. Reviewer responsibility for
|
|
24
|
+
# the harder case where a struct stores ArrayList/HashMap fields whose
|
|
25
|
+
# first append/insert happens at use-site rather than init time.
|
|
26
|
+
#
|
|
27
|
+
# Heuristics implemented:
|
|
28
|
+
# 1. Init/deinit pair existence (with allocation refinement) — BLOCKING.
|
|
29
|
+
# 2. defer X.free(buf) + errdefer X.free(buf) on same target adjacent
|
|
30
|
+
# lines — BLOCKING (per §10A.LC4).
|
|
31
|
+
# Skips trivial optional bindings (`if (X) |v| free(v)`) where the
|
|
32
|
+
# captured name is a single 1-2 char identifier — those are loop-
|
|
33
|
+
# style bindings, not the same allocation.
|
|
34
|
+
#
|
|
35
|
+
# Heuristics deferred to reviewer:
|
|
36
|
+
# - Errdefer placement (last errdefer must precede last protected alloc).
|
|
37
|
+
# - Idempotency of deinit (requires test inspection).
|
|
38
|
+
# - Arena leakage into long-lived structs.
|
|
39
|
+
# - ArrayList/HashMap field with no init-time alloc but use-time growth.
|
|
40
|
+
#
|
|
41
|
+
# Modes:
|
|
42
|
+
# --staged diff-scope: only files in `git diff --cached`
|
|
43
|
+
# --all (default) src/ scan including *_test.zig
|
|
44
|
+
#
|
|
45
|
+
# Exits 0 clean, 1 on blocking findings.
|
|
46
|
+
|
|
47
|
+
set -euo pipefail
|
|
48
|
+
|
|
49
|
+
MODE="${1:-${SCOPE:-all}}"
|
|
50
|
+
ROOT="$(git rev-parse --show-toplevel)"
|
|
51
|
+
cd "$ROOT"
|
|
52
|
+
|
|
53
|
+
FAIL=0
|
|
54
|
+
fail() { printf "FAIL: %s\n" "$*" >&2; FAIL=1; }
|
|
55
|
+
ok() { printf "OK: %s\n" "$*"; }
|
|
56
|
+
note() { printf "NOTE: %s\n" "$*"; }
|
|
57
|
+
|
|
58
|
+
# ---------------------------------------------------------------------------
|
|
59
|
+
# 1. Gather Zig files in scope. Tests INCLUDED — same lifecycle rules apply.
|
|
60
|
+
# ---------------------------------------------------------------------------
|
|
61
|
+
case "$MODE" in
|
|
62
|
+
--staged|staged)
|
|
63
|
+
mapfile -t FILES < <(git diff --cached --name-only --diff-filter=ACMRT \
|
|
64
|
+
| grep -E '^src/.*\.zig$' || true)
|
|
65
|
+
;;
|
|
66
|
+
--all|all)
|
|
67
|
+
mapfile -t FILES < <(find src -type f -name '*.zig' 2>/dev/null || true)
|
|
68
|
+
;;
|
|
69
|
+
*)
|
|
70
|
+
printf "usage: %s [--staged|--all]\n" "$0" >&2
|
|
71
|
+
exit 64
|
|
72
|
+
;;
|
|
73
|
+
esac
|
|
74
|
+
|
|
75
|
+
if [[ ${#FILES[@]} -eq 0 ]]; then
|
|
76
|
+
ok "no zig source files in scope ($MODE)"
|
|
77
|
+
exit 0
|
|
78
|
+
fi
|
|
79
|
+
|
|
80
|
+
# ---------------------------------------------------------------------------
|
|
81
|
+
# Helper regexes.
|
|
82
|
+
# ---------------------------------------------------------------------------
|
|
83
|
+
CLEANUP_RE='pub fn (deinit|close|release|destroy|shutdown|dispose|free)\('
|
|
84
|
+
# Heap-pointer return on init signature: `) *T` or `) !*T` or returning *Self.
|
|
85
|
+
HEAP_RETURN_RE='\)[[:space:]]*!?\*([A-Z@]|Self\b)'
|
|
86
|
+
# Allocation patterns inside init body.
|
|
87
|
+
ALLOC_RE='\.(alloc|create|dupe|dupeZ|initCapacity|allocPrint|allocSentinel|reAlloc|realloc)\(|std\.fs\.(File|cwd\(\))\.(open|create)|Thread\.spawn|\.allocator\(\)|ArrayList(Unmanaged)?\(.*\)\.init|HashMap(Unmanaged)?\(.*\)\.init|StringHashMap(Unmanaged)?\(.*\)\.init'
|
|
88
|
+
|
|
89
|
+
# ---------------------------------------------------------------------------
|
|
90
|
+
# 2. Per-init evaluation. Each `pub fn init` is checked individually:
|
|
91
|
+
# look at the signature for heap-return; look at next ~50 lines of body
|
|
92
|
+
# for allocation. Either positive triggers the deinit requirement.
|
|
93
|
+
# ---------------------------------------------------------------------------
|
|
94
|
+
files_scanned=0
|
|
95
|
+
inits_total=0
|
|
96
|
+
inits_value_type=0
|
|
97
|
+
inits_requires_cleanup=0
|
|
98
|
+
inits_unpaired=0
|
|
99
|
+
|
|
100
|
+
# Filter to existing files once.
|
|
101
|
+
present_files=()
|
|
102
|
+
for f in "${FILES[@]}"; do
|
|
103
|
+
[[ -f "$f" ]] && present_files+=("$f") && files_scanned=$((files_scanned + 1))
|
|
104
|
+
done
|
|
105
|
+
|
|
106
|
+
# M70 perf: pre-compute file sets in batched greps instead of one grep
|
|
107
|
+
# per file (~760 forks → 2 forks). Reads as "membership sets" for the
|
|
108
|
+
# per-init loop below.
|
|
109
|
+
declare -A files_with_cleanup=()
|
|
110
|
+
if [[ ${#present_files[@]} -gt 0 ]]; then
|
|
111
|
+
while IFS= read -r f; do
|
|
112
|
+
[[ -n "$f" ]] && files_with_cleanup["$f"]=1
|
|
113
|
+
done < <(grep -lE "$CLEANUP_RE" "${present_files[@]}" 2>/dev/null || true)
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
# Enumerate every `pub fn init(` site in one batched grep. Output shape:
|
|
117
|
+
# `<file>:<line>:<sig>` — same as the per-file `grep -nE` produced, just
|
|
118
|
+
# with the filename prepended by `-H`.
|
|
119
|
+
inits_list=$(grep -nHE 'pub fn init\(' "${present_files[@]}" 2>/dev/null || true)
|
|
120
|
+
|
|
121
|
+
if [[ -n "$inits_list" ]]; then
|
|
122
|
+
while IFS= read -r row; do
|
|
123
|
+
[[ -z "$row" ]] && continue
|
|
124
|
+
f="${row%%:*}"
|
|
125
|
+
rest="${row#*:}"
|
|
126
|
+
ln="${rest%%:*}"
|
|
127
|
+
sig="${rest#*:}"
|
|
128
|
+
inits_total=$((inits_total + 1))
|
|
129
|
+
|
|
130
|
+
heap=0
|
|
131
|
+
if [[ "$sig" =~ \)[[:space:]]*\!?\*([A-Z@]|Self) ]]; then
|
|
132
|
+
heap=1
|
|
133
|
+
fi
|
|
134
|
+
|
|
135
|
+
# Body window: from sig line to next pub-fn / fn / EOF, capped at +50.
|
|
136
|
+
body=$(awk -v start="$ln" '
|
|
137
|
+
NR <= start { next }
|
|
138
|
+
/^[[:space:]]*(pub )?fn [a-zA-Z_]/ { exit }
|
|
139
|
+
NR > start + 50 { exit }
|
|
140
|
+
{ print }
|
|
141
|
+
' "$f")
|
|
142
|
+
|
|
143
|
+
allocates=0
|
|
144
|
+
if grep -qE "$ALLOC_RE" <<<"$body"; then
|
|
145
|
+
allocates=1
|
|
146
|
+
fi
|
|
147
|
+
|
|
148
|
+
if [[ $heap -eq 0 && $allocates -eq 0 ]]; then
|
|
149
|
+
inits_value_type=$((inits_value_type + 1))
|
|
150
|
+
continue
|
|
151
|
+
fi
|
|
152
|
+
|
|
153
|
+
inits_requires_cleanup=$((inits_requires_cleanup + 1))
|
|
154
|
+
if [[ -z "${files_with_cleanup[$f]+x}" ]]; then
|
|
155
|
+
inits_unpaired=$((inits_unpaired + 1))
|
|
156
|
+
reason=""
|
|
157
|
+
[[ $heap -eq 1 ]] && reason="heap-return"
|
|
158
|
+
[[ $allocates -eq 1 ]] && reason="${reason:+$reason+}allocates"
|
|
159
|
+
fail "$f:$ln — \`pub fn init\` requires cleanup ($reason) but no cleanup method in file"
|
|
160
|
+
printf " > %s\n" "$(echo "$sig" | sed 's/^[[:space:]]*//')" >&2
|
|
161
|
+
fi
|
|
162
|
+
done <<< "$inits_list"
|
|
163
|
+
fi
|
|
164
|
+
|
|
165
|
+
# ---------------------------------------------------------------------------
|
|
166
|
+
# 3. Detect defer/errdefer conflicts on the same allocation target.
|
|
167
|
+
# Skip trivial optional-binding names (1-2 char like `v`, `it`) which
|
|
168
|
+
# are local to each `if (x) |v|` scope and not actual conflicts.
|
|
169
|
+
# ---------------------------------------------------------------------------
|
|
170
|
+
# M70 perf: single awk across every file. File-local state (defers[]
|
|
171
|
+
# and errdefers[] arrays) resets on FNR == 1 via process_file() flush.
|
|
172
|
+
if [[ ${#present_files[@]} -gt 0 ]]; then
|
|
173
|
+
while IFS= read -r line; do
|
|
174
|
+
[[ -z "$line" ]] && continue
|
|
175
|
+
fail "defer/errdefer conflict (same target, ≤4 line window): $line"
|
|
176
|
+
done < <(awk '
|
|
177
|
+
function extract_target(line, s, p, q) {
|
|
178
|
+
p = index(line, ".free(");
|
|
179
|
+
if (p == 0) p = index(line, ".destroy(");
|
|
180
|
+
if (p == 0) return "";
|
|
181
|
+
s = substr(line, p);
|
|
182
|
+
p = index(s, "(");
|
|
183
|
+
q = index(s, ")");
|
|
184
|
+
if (p == 0 || q == 0 || q <= p + 1) return "";
|
|
185
|
+
return substr(s, p + 1, q - p - 1);
|
|
186
|
+
}
|
|
187
|
+
function is_optional_binding(line) {
|
|
188
|
+
return (line ~ /\|[a-z_]+\|/);
|
|
189
|
+
}
|
|
190
|
+
function flush() {
|
|
191
|
+
for (d in defers) {
|
|
192
|
+
for (e in errdefers) {
|
|
193
|
+
gap = d - e; if (gap < 0) gap = -gap;
|
|
194
|
+
if (defers[d] == errdefers[e] && gap <= 4) {
|
|
195
|
+
printf "%s:%d:%d: defer/errdefer conflict on %s\n", current_file, d, e, defers[d];
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
delete defers; delete errdefers
|
|
200
|
+
}
|
|
201
|
+
FNR == 1 && current_file != "" { flush() }
|
|
202
|
+
FNR == 1 { current_file = FILENAME }
|
|
203
|
+
/^[[:space:]]*defer[[:space:]].*\.(free|destroy)\(/ {
|
|
204
|
+
tgt = extract_target($0);
|
|
205
|
+
if (tgt != "" && length(tgt) > 2 && !is_optional_binding($0)) defers[FNR] = tgt;
|
|
206
|
+
}
|
|
207
|
+
/^[[:space:]]*errdefer[[:space:]].*\.(free|destroy)\(/ {
|
|
208
|
+
tgt = extract_target($0);
|
|
209
|
+
if (tgt != "" && length(tgt) > 2 && !is_optional_binding($0)) errdefers[FNR] = tgt;
|
|
210
|
+
}
|
|
211
|
+
END { if (current_file != "") flush() }
|
|
212
|
+
' "${present_files[@]}")
|
|
213
|
+
fi
|
|
214
|
+
|
|
215
|
+
# ---------------------------------------------------------------------------
|
|
216
|
+
# 4. Verdict.
|
|
217
|
+
# ---------------------------------------------------------------------------
|
|
218
|
+
ok "scanned $files_scanned files; $inits_total init methods ($inits_value_type value-type, $inits_requires_cleanup require cleanup, $inits_unpaired unpaired)"
|
|
219
|
+
if [[ $FAIL -ne 0 ]]; then
|
|
220
|
+
printf "\n🔴 LIFECYCLE GATE: violations found. See dispatch/write_zig.md (lifecycle).\n" >&2
|
|
221
|
+
exit 1
|
|
222
|
+
fi
|
|
223
|
+
ok "LIFECYCLE GATE: clean"
|
|
224
|
+
exit 0
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# design-tokens.sh — enforce design-system token discipline across
|
|
3
|
+
# ui/packages/{app,website}/**/*.tsx.
|
|
4
|
+
#
|
|
5
|
+
# Dispatch façade: dispatch/write_ts_adhere_bun.md (Design Tokens / DESIGN TOKEN GATE)
|
|
6
|
+
# Fires in: CONFORM (via `make harness-verify` in `agentsfleet`).
|
|
7
|
+
#
|
|
8
|
+
# Rejects arbitrary Tailwind classes when an equivalent design-system
|
|
9
|
+
# token utility exists in ui/packages/design-system/src/theme.css.
|
|
10
|
+
# The token set is encoded below; when the design system adds a token,
|
|
11
|
+
# add the matching grep here in the same commit.
|
|
12
|
+
#
|
|
13
|
+
# TECHNICAL DEBT (acknowledged on migration to dotfiles, 2026-05-11):
|
|
14
|
+
# The PATTERNS array hard-codes agentsfleet-specific token names
|
|
15
|
+
# (text-fluid-hero, max-w-prose, tracking-display-md, etc.). If a second
|
|
16
|
+
# project adopts this dotfiles set, it must either:
|
|
17
|
+
# (a) share agentsfleet's exact token vocabulary, or
|
|
18
|
+
# (b) fork the PATTERNS array, or
|
|
19
|
+
# (c) wait for the parameterised version that reads token names from
|
|
20
|
+
# $REPO/ui/packages/design-system/src/theme.css at runtime.
|
|
21
|
+
# Path (c) is the right long-term shape; not done yet.
|
|
22
|
+
#
|
|
23
|
+
# Scope (M70):
|
|
24
|
+
# Walks the full working tree via `git ls-files` — sees staged content
|
|
25
|
+
# because the index is what `ls-files` reports. Pre-commit-safe: a fix
|
|
26
|
+
# staged but not yet committed satisfies the check on the same hook run.
|
|
27
|
+
# The previous `--diff` (BASE...HEAD) default was retired with M70
|
|
28
|
+
# because it was blind to the index at pre-commit time.
|
|
29
|
+
#
|
|
30
|
+
# Modes:
|
|
31
|
+
# (no flag) / --all full-codebase scan via `git ls-files` (default)
|
|
32
|
+
# --staged narrow to files in `git diff --cached --name-only`
|
|
33
|
+
# — opt-in fast loop for iterative dev
|
|
34
|
+
#
|
|
35
|
+
# Exits 0 clean, 1 on any violation. Test files exempt
|
|
36
|
+
# (snapshot/E2E assert on rendered DOM).
|
|
37
|
+
|
|
38
|
+
set -euo pipefail
|
|
39
|
+
|
|
40
|
+
MODE="${1:---all}"
|
|
41
|
+
ROOT="$(git rev-parse --show-toplevel)"
|
|
42
|
+
cd "$ROOT"
|
|
43
|
+
|
|
44
|
+
# ── Patterns ─────────────────────────────────────────────────────────────
|
|
45
|
+
#
|
|
46
|
+
# Each pattern flags an arbitrary CSS class that has a token equivalent.
|
|
47
|
+
# Format: <regex>|<suggestion>. The suggestion column is hint-only — the
|
|
48
|
+
# closest token depends on context; the audit just blocks the arbitrary.
|
|
49
|
+
#
|
|
50
|
+
# Tailwind base utilities (m-0, w-full, flex, gap-N, etc.) are not flagged
|
|
51
|
+
# — only arbitrary-value classes (the `text-[...]`, `leading-[...]`,
|
|
52
|
+
# `max-w-[...]`, `tracking-[...]` shape).
|
|
53
|
+
|
|
54
|
+
# Format: <regex>:::<suggestion>. The `:::` delimiter is unambiguous —
|
|
55
|
+
# regex content frequently contains `|` (alternation), so we can't use it.
|
|
56
|
+
PATTERNS=(
|
|
57
|
+
'text-\[[0-9]+(px|rem)\]:::text-{label,eyebrow,body-sm,body,body-lg,heading,display-md,display-lg,display-xl}'
|
|
58
|
+
'text-\[clamp\(:::text-fluid-{display-md,display-lg,hero}'
|
|
59
|
+
'tracking-\[-?[0-9]:::tracking-{display-xl,display-lg,display-md,eyebrow,label}'
|
|
60
|
+
'leading-\[[0-9]:::leading-{display-xl,display-lg,display-md,heading,eyebrow,body-lg,body,body-sm,label,mono,prose}'
|
|
61
|
+
'max-w-\[[0-9]+(px|rem|ch)\]:::max-w-{trim,narrow,measure,form,wide,content,tagline,prose}'
|
|
62
|
+
'(text|bg|border)-(red|blue|green|yellow|purple|indigo|violet|pink|orange|gray|slate|zinc|neutral)-[0-9]:::use semantic tokens (text-destructive, text-error, text-success, …)'
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# ── File scope ───────────────────────────────────────────────────────────
|
|
66
|
+
in_scope() {
|
|
67
|
+
local f="$1"
|
|
68
|
+
case "$f" in
|
|
69
|
+
*.test.tsx|*.test.jsx) return 1 ;;
|
|
70
|
+
*tests/e2e/*) return 1 ;;
|
|
71
|
+
ui/packages/app/*.tsx|ui/packages/app/*.jsx) return 0 ;;
|
|
72
|
+
ui/packages/website/*.tsx|ui/packages/website/*.jsx) return 0 ;;
|
|
73
|
+
ui/packages/app/**/*.tsx|ui/packages/app/**/*.jsx) return 0 ;;
|
|
74
|
+
ui/packages/website/**/*.tsx|ui/packages/website/**/*.jsx) return 0 ;;
|
|
75
|
+
*) return 1 ;;
|
|
76
|
+
esac
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
case "$MODE" in
|
|
80
|
+
--staged|staged)
|
|
81
|
+
FILES=$(git diff --cached --name-only --diff-filter=ACMRT 2>/dev/null || true)
|
|
82
|
+
;;
|
|
83
|
+
--all|all)
|
|
84
|
+
# `git ls-files` reports the index, which includes staged content —
|
|
85
|
+
# so pre-commit-style invocations see staged-but-not-committed files
|
|
86
|
+
# without needing a `--staged` flag.
|
|
87
|
+
FILES=$(git ls-files -- 'ui/packages/app/*.tsx' 'ui/packages/app/*.jsx' 'ui/packages/website/*.tsx' 'ui/packages/website/*.jsx' 2>/dev/null || true)
|
|
88
|
+
;;
|
|
89
|
+
*)
|
|
90
|
+
echo "usage: $0 [--all|--staged]" >&2
|
|
91
|
+
echo "note: --diff was retired in M70 — see dispatch/write_ts_adhere_bun.md (Design Tokens → Scope)." >&2
|
|
92
|
+
exit 2 ;;
|
|
93
|
+
esac
|
|
94
|
+
|
|
95
|
+
FAIL=0
|
|
96
|
+
SEEN_FILES_LIST=""
|
|
97
|
+
|
|
98
|
+
# Pre-filter FILES list once (drop blanks, non-existent, out-of-scope).
|
|
99
|
+
SCOPED_FILES=()
|
|
100
|
+
while IFS= read -r f; do
|
|
101
|
+
[ -z "$f" ] && continue
|
|
102
|
+
[ ! -f "$f" ] && continue
|
|
103
|
+
in_scope "$f" || continue
|
|
104
|
+
SCOPED_FILES+=("$f")
|
|
105
|
+
done <<< "$FILES"
|
|
106
|
+
|
|
107
|
+
if [ "${#SCOPED_FILES[@]}" -gt 0 ]; then
|
|
108
|
+
for entry in "${PATTERNS[@]}"; do
|
|
109
|
+
regex="${entry%%:::*}"
|
|
110
|
+
suggestion="${entry#*:::}"
|
|
111
|
+
# Single grep across all scoped files per pattern.
|
|
112
|
+
# `grep -nHE` always prefixes with filename + line; `-H` is required
|
|
113
|
+
# when a single file is supplied so the parser sees a uniform shape.
|
|
114
|
+
while IFS= read -r match; do
|
|
115
|
+
[ -z "$match" ] && continue
|
|
116
|
+
f="${match%%:*}"
|
|
117
|
+
rest="${match#*:}"
|
|
118
|
+
ln="${rest%%:*}"
|
|
119
|
+
# Honor inline override `// DESIGN TOKEN: SKIPPED per user override (reason: ...)`
|
|
120
|
+
# by checking the matched line and the line above.
|
|
121
|
+
ctx=$(awk -v n="$ln" 'NR>=n-1 && NR<=n' "$f" 2>/dev/null || true)
|
|
122
|
+
if printf '%s' "$ctx" | grep -q 'DESIGN TOKEN: SKIPPED'; then continue; fi
|
|
123
|
+
printf '%s\n -> %s\n' "$match" "$suggestion"
|
|
124
|
+
SEEN_FILES_LIST="${SEEN_FILES_LIST}${f}"$'\n'
|
|
125
|
+
FAIL=1
|
|
126
|
+
done < <(grep -nHE "$regex" "${SCOPED_FILES[@]}" 2>/dev/null || true)
|
|
127
|
+
done
|
|
128
|
+
fi
|
|
129
|
+
|
|
130
|
+
FILES_WITH_VIOLATIONS=$(printf '%s' "$SEEN_FILES_LIST" | sort -u | grep -c . || true)
|
|
131
|
+
|
|
132
|
+
if [ "$FAIL" = "0" ]; then
|
|
133
|
+
echo "OK: design-token discipline — no arbitraries that have a token equivalent (mode=$MODE)"
|
|
134
|
+
exit 0
|
|
135
|
+
fi
|
|
136
|
+
|
|
137
|
+
echo
|
|
138
|
+
echo "FAIL: design-token discipline ($FILES_WITH_VIOLATIONS file(s) with violations)"
|
|
139
|
+
echo " Replace each match with the suggested token utility, or add the inline override:"
|
|
140
|
+
echo " // DESIGN TOKEN: SKIPPED per user override (reason: <concrete reason>)"
|
|
141
|
+
echo " Token source: ui/packages/design-system/src/theme.css"
|
|
142
|
+
echo " Rule body: dispatch/write_ts_adhere_bun.md (Design Tokens)"
|
|
143
|
+
exit 1
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# doc-read.sh — turn the 📖 DOC READ proof-line into a set comparison.
|
|
3
|
+
#
|
|
4
|
+
# audits/doc-read.sh log <path> record that a file was read (hook target)
|
|
5
|
+
# audits/doc-read.sh check staged source vs recorded reads
|
|
6
|
+
#
|
|
7
|
+
# The DOC READ GATE asks an agent to read the façade its edit triggers, and
|
|
8
|
+
# then asks the same agent whether it did. That is a claim about itself
|
|
9
|
+
# compared against nothing. `log` records the read and `check` compares the
|
|
10
|
+
# record against the façades the staged files trigger.
|
|
11
|
+
#
|
|
12
|
+
# NOTHING HERE MAY DEPEND ON AN AGENT RUNTIME. `log` is a shell command any
|
|
13
|
+
# agent can run; a runtime that also has a read hook (Claude Code today) can
|
|
14
|
+
# call it automatically, and a runtime that removes that feature tomorrow
|
|
15
|
+
# changes nothing — the agent runs the command, the record is identical, the
|
|
16
|
+
# gate behaves the same. Vendor-shaped configuration stays out of this file and
|
|
17
|
+
# out of .githooks/, and an eval fails the build if it appears
|
|
18
|
+
# (evals/ledger/run.sh, ledger_doc_read_runtime_neutral).
|
|
19
|
+
#
|
|
20
|
+
# The record lives in the git directory, never in the tree: it is evidence
|
|
21
|
+
# about one working copy, not shared history. When it is absent — an agent
|
|
22
|
+
# runtime with no hook support — `check` warns and exits 0. Partial
|
|
23
|
+
# mechanization stated honestly beats a red that means nothing.
|
|
24
|
+
#
|
|
25
|
+
# Exit: 0 clean or unenforceable · 1 unread trigger · 2 usage error.
|
|
26
|
+
set -uo pipefail
|
|
27
|
+
|
|
28
|
+
# A hook exports GIT_DIR/GIT_INDEX_FILE at the repository it fired in. This
|
|
29
|
+
# script IS a hook target, so the scope dies before the first git call.
|
|
30
|
+
unset GIT_DIR GIT_INDEX_FILE GIT_WORK_TREE GIT_COMMON_DIR GIT_PREFIX \
|
|
31
|
+
GIT_OBJECT_DIRECTORY GIT_ALTERNATE_OBJECT_DIRECTORIES
|
|
32
|
+
|
|
33
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
34
|
+
# shellcheck source=audits/rule-ledger-lib.sh
|
|
35
|
+
source "$HERE/rule-ledger-lib.sh"
|
|
36
|
+
|
|
37
|
+
MODE_LOG="log"
|
|
38
|
+
MODE_CHECK="check"
|
|
39
|
+
LOG_RELATIVE_PATH="orly/doc-reads.jsonl"
|
|
40
|
+
USAGE="usage: $0 log <path> | $0 check"
|
|
41
|
+
|
|
42
|
+
if [[ -t 1 ]]; then G=$'\033[32m'; R=$'\033[31m'; Y=$'\033[33m'; X=$'\033[0m'
|
|
43
|
+
else G=''; R=''; Y=''; X=''; fi
|
|
44
|
+
|
|
45
|
+
# Where the record lives for this working copy. Empty when the caller is not
|
|
46
|
+
# inside a repository at all, which only `log` tolerates.
|
|
47
|
+
read_log_path() {
|
|
48
|
+
local git_dir
|
|
49
|
+
git_dir="$(git -C "$LEDGER_ROOT" rev-parse --absolute-git-dir 2>/dev/null)" || return 1
|
|
50
|
+
printf '%s/%s' "$git_dir" "$LOG_RELATIVE_PATH"
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
# Repo-relative form of a path, or nothing when it sits outside the tree — an
|
|
54
|
+
# agent reading its own notes elsewhere on disk is not a doc read.
|
|
55
|
+
repo_relative() {
|
|
56
|
+
local path="$1"
|
|
57
|
+
case "$path" in
|
|
58
|
+
"$LEDGER_ROOT"/*) printf '%s' "${path#"$LEDGER_ROOT"/}" ;;
|
|
59
|
+
/*) return 1 ;;
|
|
60
|
+
*) [ -e "$LEDGER_ROOT/$path" ] && printf '%s' "$path" ;;
|
|
61
|
+
esac
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
# One JSON Lines row per call, appended. Append-only with a single write per
|
|
65
|
+
# invocation is why two agents in two sessions cannot corrupt each other: the
|
|
66
|
+
# order rows land in does not change which paths the set contains.
|
|
67
|
+
run_log() {
|
|
68
|
+
local path="$1" relative log blob
|
|
69
|
+
relative="$(repo_relative "$path")" || return 0
|
|
70
|
+
[ -n "$relative" ] || return 0
|
|
71
|
+
log="$(read_log_path)" || return 0
|
|
72
|
+
mkdir -p "$(dirname "$log")" 2>/dev/null || return 0
|
|
73
|
+
blob="$(content_hash "$relative")"
|
|
74
|
+
# A hash we could not compute proves nothing was read at a known version.
|
|
75
|
+
# Recording it would let two failures compare equal and validate a read of a
|
|
76
|
+
# file that is no longer there.
|
|
77
|
+
[ -n "$blob" ] || return 0
|
|
78
|
+
printf '{"ts":%s,"path":"%s","blob":"%s"}\n' "$(date +%s)" "$(json_escape "$relative")" "$blob" >> "$log"
|
|
79
|
+
return 0
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
# Backslash and double quote are legal in a POSIX filename and illegal raw in a
|
|
83
|
+
# JSON string. Unescaped, `we"ird.md` writes a row no parser can read — and this
|
|
84
|
+
# one mis-splits it, reporting a path that was never read.
|
|
85
|
+
json_escape() {
|
|
86
|
+
printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g'
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
# The content a read actually saw. Validity is keyed to this rather than to a
|
|
90
|
+
# clock: an unchanged document does not need re-reading because a commit
|
|
91
|
+
# happened, and a changed one voids every prior read the same second it is
|
|
92
|
+
# edited — which a timestamp window silently fails to do.
|
|
93
|
+
content_hash() {
|
|
94
|
+
git -C "$LEDGER_ROOT" hash-object "$LEDGER_ROOT/$1" 2>/dev/null || printf ''
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
# Façade pages the staged files trigger, one per line. Same glob map the
|
|
98
|
+
# reachability report uses: a façade's scope is its dispatch_init line, so this
|
|
99
|
+
# check and the ledger can never disagree about what fires.
|
|
100
|
+
expected_facade_pages() {
|
|
101
|
+
local staged="$1" script stem globs
|
|
102
|
+
while IFS= read -r script; do
|
|
103
|
+
[ -n "$script" ] || continue
|
|
104
|
+
stem="$(basename "$script" .sh)"
|
|
105
|
+
[ -f "$LEDGER_ROOT/dispatch/$stem.md" ] || continue
|
|
106
|
+
globs="$(ledger_facade_globs "$script")"
|
|
107
|
+
[ -n "$globs" ] || continue
|
|
108
|
+
[ "$(ledger_match_count "$globs" < "$staged")" -gt 0 ] && printf 'dispatch/%s.md\n' "$stem"
|
|
109
|
+
done < <(ledger_facade_scripts)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
json_unescape() {
|
|
113
|
+
printf '%s' "$1" | sed -e 's/\\"/"/g' -e 's/\\\\/\\/g'
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
# Paths whose recorded read saw the content that is on disk now. A row written
|
|
117
|
+
# against an older version of the document proves the agent read something
|
|
118
|
+
# else, so it does not count — and rows from before this milestone, which carry
|
|
119
|
+
# no blob, cannot prove anything and are ignored.
|
|
120
|
+
current_reads() {
|
|
121
|
+
local log="$1" line path blob
|
|
122
|
+
while IFS= read -r line; do
|
|
123
|
+
case "$line" in *'"blob":"'*) ;; *) continue ;; esac
|
|
124
|
+
blob="${line##*\"blob\":\"}"; blob="${blob%%\"*}"
|
|
125
|
+
[ -n "$blob" ] || continue
|
|
126
|
+
path="${line#*\"path\":\"}"; path="${path%\",\"blob\":\"$blob\"\}}"
|
|
127
|
+
path="$(json_unescape "$path")"
|
|
128
|
+
[ "$blob" = "$(content_hash "$path")" ] && printf '%s\n' "$path"
|
|
129
|
+
done < "$log"
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
# File-scope so the EXIT trap can still see them: a trap fires after a
|
|
133
|
+
# function's locals are gone, and `set -u` turns that into a spurious error on
|
|
134
|
+
# the way out of a clean run.
|
|
135
|
+
STAGED_FILE=""
|
|
136
|
+
EXPECTED_FILE=""
|
|
137
|
+
RECORDED_FILE=""
|
|
138
|
+
cleanup_temporaries() { rm -f "$STAGED_FILE" "$EXPECTED_FILE" "$RECORDED_FILE"; }
|
|
139
|
+
|
|
140
|
+
run_check() {
|
|
141
|
+
local log unread=0 page staged expected recorded
|
|
142
|
+
STAGED_FILE="$(mktemp)"; EXPECTED_FILE="$(mktemp)"; RECORDED_FILE="$(mktemp)"
|
|
143
|
+
staged="$STAGED_FILE"; expected="$EXPECTED_FILE"; recorded="$RECORDED_FILE"
|
|
144
|
+
trap cleanup_temporaries EXIT
|
|
145
|
+
git -C "$LEDGER_ROOT" diff --cached --name-only --diff-filter=ACMRT > "$staged" 2>/dev/null
|
|
146
|
+
expected_facade_pages "$staged" | sort -u > "$expected"
|
|
147
|
+
|
|
148
|
+
if [ ! -s "$expected" ]; then
|
|
149
|
+
printf ' %s🟢%s DOC READ: nothing staged triggers a façade\n' "$G" "$X"
|
|
150
|
+
return 0
|
|
151
|
+
fi
|
|
152
|
+
if ! log="$(read_log_path)" || [ ! -f "$log" ]; then
|
|
153
|
+
printf ' %s🟠%s DOC READ: no read record for this working copy — the agent runtime\n' "$Y" "$X"
|
|
154
|
+
printf ' has no Read hook, so the proof-line stays self-reported here\n'
|
|
155
|
+
return 0
|
|
156
|
+
fi
|
|
157
|
+
|
|
158
|
+
# Materialised once, and never piped into `grep -q`: under `pipefail` the
|
|
159
|
+
# producer takes SIGPIPE the moment grep matches and exits, and the pipeline
|
|
160
|
+
# then reports 141 — which reads exactly like "not found" and reds a doc that
|
|
161
|
+
# was read.
|
|
162
|
+
current_reads "$log" | sort -u > "$recorded"
|
|
163
|
+
|
|
164
|
+
while IFS= read -r page; do
|
|
165
|
+
grep -qxF "$page" "$recorded" && continue
|
|
166
|
+
printf ' %s🔴%s DOC READ: %s triggered by the staged diff, not read at its current content\n' "$R" "$X" "$page"
|
|
167
|
+
unread=1
|
|
168
|
+
done < "$expected"
|
|
169
|
+
[ "$unread" -eq 0 ] && printf ' %s🟢%s DOC READ: every triggered façade was read at its current content\n' "$G" "$X"
|
|
170
|
+
return "$unread"
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
case "${1:-}" in
|
|
174
|
+
"$MODE_LOG")
|
|
175
|
+
[ "$#" -eq 2 ] || { printf '%s\n' "$USAGE" >&2; exit 2; }
|
|
176
|
+
run_log "$2"
|
|
177
|
+
;;
|
|
178
|
+
"$MODE_CHECK")
|
|
179
|
+
[ "$#" -eq 1 ] || { printf '%s\n' "$USAGE" >&2; exit 2; }
|
|
180
|
+
run_check
|
|
181
|
+
;;
|
|
182
|
+
*) printf '%s\n' "$USAGE" >&2; exit 2 ;;
|
|
183
|
+
esac
|