@agentsfleet/orly 0.10.13 → 0.11.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 (155) hide show
  1. package/.githooks/pre-push +2 -1
  2. package/AGENTS.md +21 -31
  3. package/README.md +65 -29
  4. package/SOUL.md +13 -23
  5. package/audits/agents-md.md +67 -57
  6. package/audits/deinit-pairs.sh +13 -18
  7. package/audits/design-tokens.sh +9 -23
  8. package/audits/doc-read.sh +10 -33
  9. package/audits/doc-reads.ts +41 -0
  10. package/audits/error-codes.sh +18 -25
  11. package/audits/logging-rust.awk +145 -0
  12. package/audits/logging.sh +10 -166
  13. package/audits/msid-ui.sh +45 -18
  14. package/audits/rule-ledger-lib.sh +9 -6
  15. package/audits/rust-error.sh +20 -15
  16. package/audits/scope.sh +88 -0
  17. package/audits/spec-template.sh +2 -2
  18. package/audits/spec-template.ts +1 -1
  19. package/audits/sql-mod.sh +11 -14
  20. package/audits/ufs-strings.awk +164 -0
  21. package/audits/ufs.sh +10 -188
  22. package/core/operating-model.md +8 -8
  23. package/dispatch/edit_rules.md +8 -6
  24. package/dispatch/lib.sh +41 -26
  25. package/dispatch/lifecycle.md +18 -6
  26. package/dispatch/name_architecture.md +8 -3
  27. package/dispatch/verify.md +1 -1
  28. package/dispatch/write_any.md +10 -3
  29. package/dispatch/write_changelog.md +7 -4
  30. package/dispatch/write_go.md +1 -1
  31. package/dispatch/write_http.md +5 -7
  32. package/dispatch/write_python.md +1 -1
  33. package/dispatch/write_rust.md +17 -1
  34. package/dispatch/write_shell.md +1 -1
  35. package/dispatch/write_spec.md +3 -1
  36. package/dispatch/write_sql.md +28 -6
  37. package/dispatch/write_ts_adhere_bun.md +11 -0
  38. package/dispatch/write_zig.md +53 -21
  39. package/docs/CHANGELOG_VOICE.md +11 -2
  40. package/docs/EXECUTE_DOC_READS.md +10 -10
  41. package/docs/HARNESS_VERIFY_OUTPUT.md +6 -3
  42. package/docs/JUDGMENTS.md +153 -0
  43. package/docs/LIFECYCLE_PATTERNS.md +26 -16
  44. package/docs/LOGGING_STANDARD.md +39 -19
  45. package/docs/ORLY_ARCHITECTURE.md +77 -6
  46. package/docs/RELEASE_TEMPLATE.md +21 -5
  47. package/docs/REST_API_DESIGN_GUIDELINES.md +86 -21
  48. package/docs/RULE_ENFORCEMENT.md +6 -6
  49. package/docs/SCHEMA_CONVENTIONS.md +24 -3
  50. package/docs/TEMPLATE.md +4 -4
  51. package/docs/VERIFY_TIERS.md +2 -2
  52. package/docs/greptile-learnings/RULES.md +81 -26
  53. package/llms.txt +37 -34
  54. package/package.json +6 -1
  55. package/packs/language/go/rules.md +1 -1
  56. package/packs/language/python/rules.md +1 -1
  57. package/packs/language/rust/rules.md +17 -1
  58. package/packs/language/shell/rules.md +1 -1
  59. package/registry.json +22 -7
  60. package/schemas/profile.schema.json +48 -5
  61. package/skills/orly-babysit-prs/SKILL.md +87 -34
  62. package/skills/orly-spec-new/SKILL.md +19 -6
  63. package/skills/orly-write-integration-test/SKILL.md +4 -4
  64. package/skills/orly-write-unit-test/SKILL.md +10 -1
  65. package/src/cli.test.ts +2 -2
  66. package/src/cli.ts +19 -75
  67. package/src/cli_help.ts +69 -0
  68. package/src/command_limits.ts +20 -0
  69. package/src/command_process.test.ts +109 -0
  70. package/src/command_process.ts +88 -0
  71. package/src/command_runner.test.ts +88 -0
  72. package/src/command_runner.ts +33 -0
  73. package/src/comprehension_cache.test.ts +123 -0
  74. package/src/config.test.ts +5 -4
  75. package/src/config.ts +26 -164
  76. package/src/config_discovery.ts +159 -0
  77. package/src/config_validation.test.ts +56 -0
  78. package/src/config_validation.ts +25 -0
  79. package/src/criteria.test.ts +1 -1
  80. package/src/criteria.ts +10 -26
  81. package/src/criteria_spec.ts +12 -20
  82. package/src/criteria_support.ts +2 -19
  83. package/src/doc_reads.test.ts +42 -0
  84. package/src/doc_rules.ts +2 -2
  85. package/src/evaluation_resources.test.ts +64 -0
  86. package/src/execution/config.ts +22 -0
  87. package/src/execution/plan.test.ts +65 -0
  88. package/src/execution/plan.ts +51 -0
  89. package/src/gates.ts +39 -24
  90. package/src/gates_spec.test.ts +110 -4
  91. package/src/gates_test_support.ts +9 -7
  92. package/src/install.test.ts +38 -222
  93. package/src/install.ts +117 -308
  94. package/src/install_loaders.test.ts +203 -0
  95. package/src/install_nested.test.ts +2 -2
  96. package/src/install_packs.test.ts +66 -6
  97. package/src/installation/hooks.ts +63 -0
  98. package/src/installation/lock.test.ts +97 -0
  99. package/src/installation/lock.ts +56 -0
  100. package/src/installation/migration.test.ts +212 -0
  101. package/src/installation/migration.ts +65 -0
  102. package/src/installation/plan.ts +96 -0
  103. package/src/installation/transaction.ts +126 -0
  104. package/src/installation_preservation.test.ts +48 -0
  105. package/src/judgments/advice.test.ts +38 -0
  106. package/src/judgments/advice.ts +32 -0
  107. package/src/judgments/command.integration.test.ts +47 -0
  108. package/src/judgments/command.ts +90 -0
  109. package/src/judgments/constants.ts +55 -0
  110. package/src/judgments/evidence.test.ts +48 -0
  111. package/src/judgments/evidence.ts +34 -0
  112. package/src/judgments/files.test.ts +62 -0
  113. package/src/judgments/files.ts +73 -0
  114. package/src/judgments/questions.test.ts +19 -0
  115. package/src/judgments/questions.ts +85 -0
  116. package/src/judgments/replay.test.ts +43 -0
  117. package/src/judgments/replay.ts +66 -0
  118. package/src/judgments/runner.integration.test.ts +165 -0
  119. package/src/judgments/runner.ts +97 -0
  120. package/src/judgments/scan.test.ts +19 -0
  121. package/src/judgments/scan.ts +40 -0
  122. package/src/judgments/syntax.test.ts +115 -0
  123. package/src/judgments/syntax.ts +71 -0
  124. package/src/judgments/syntax_worker.ts +70 -0
  125. package/src/judgments/test_support.ts +60 -0
  126. package/src/judgments/transport.integration.test.ts +63 -0
  127. package/src/judgments/transport.ts +35 -0
  128. package/src/judgments/types.test.ts +43 -0
  129. package/src/judgments/types.ts +66 -0
  130. package/src/judgments/wire.test.ts +48 -0
  131. package/src/judgments/wire.ts +40 -0
  132. package/src/loaders.test.ts +13 -16
  133. package/src/loaders.ts +28 -10
  134. package/src/model.test.ts +20 -3
  135. package/src/model.ts +7 -18
  136. package/src/pack_hygiene.test.ts +42 -1
  137. package/src/pack_hygiene.ts +18 -8
  138. package/src/references.ts +15 -7
  139. package/src/release_inventory.test.ts +80 -0
  140. package/src/render.test.ts +1 -1
  141. package/src/render.ts +3 -8
  142. package/src/required_scanner.test.ts +36 -0
  143. package/src/review_poll.test.ts +158 -0
  144. package/src/scanner_scope.test.ts +202 -0
  145. package/src/setup.test.ts +9 -6
  146. package/src/spec_baseline.test.ts +1 -1
  147. package/src/spec_completion.test.ts +74 -0
  148. package/src/spec_obligations.ts +64 -0
  149. package/src/spec_rehearsal.test.ts +182 -0
  150. package/src/spec_template_io.test.ts +11 -9
  151. package/src/surfaces.test.ts +2 -0
  152. package/src/surfaces.ts +1 -1
  153. package/src/validation.ts +7 -4
  154. package/src/verify.test.ts +18 -2
  155. package/src/verify.ts +7 -3
@@ -42,6 +42,10 @@
42
42
 
43
43
  set -euo pipefail
44
44
 
45
+ source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/scope.sh"
46
+ audit_scope_init --all "$@"
47
+ audit_index_snapshot "$@"
48
+
45
49
  MODE_STAGED="--staged"
46
50
  MODE_ALL="--all"
47
51
  RULE="ERR-RS"
@@ -63,7 +67,8 @@ for arg in "$@"; do
63
67
  printf 'usage: %s [--staged|--all]\n' "$0"
64
68
  exit 0
65
69
  ;;
66
- *) printf 'unknown arg: %s\n' "$arg" >&2; exit 64 ;;
70
+ -*) printf 'unknown arg: %s\n' "$arg" >&2; exit 2 ;;
71
+ *) MODE=explicit ;;
67
72
  esac
68
73
  done
69
74
 
@@ -83,18 +88,11 @@ is_non_runtime_rust_path() {
83
88
  }
84
89
 
85
90
  gather_paths() {
86
- case "$MODE" in
87
- "$MODE_STAGED")
88
- git diff --cached --name-only --diff-filter=ACMRT -- '*.rs' || true
89
- ;;
90
- "$MODE_ALL")
91
- git ls-files --cached --others --exclude-standard -- '*.rs' || true
92
- ;;
93
- esac
91
+ audit_scope_paths '*.rs'
94
92
  }
95
93
 
96
94
  FILES=()
97
- while IFS= read -r path; do
95
+ while IFS= read -r -d '' path; do
98
96
  [[ -n "$path" && -f "$path" ]] || continue
99
97
  is_non_runtime_rust_path "$path" && continue
100
98
  FILES+=("$path")
@@ -204,17 +202,24 @@ crate_root_of() {
204
202
  }
205
203
 
206
204
  alias_count=0
207
- checked_crates=""
205
+ checked_crates=()
208
206
  for path in ${error_type_files[@]+"${error_type_files[@]}"}; do
209
207
  crate=""
210
208
  crate_root_of "$path" > /dev/null 2>&1 && crate="$(crate_root_of "$path")"
211
209
  # A .rs file outside any Cargo crate has no crate to carry the alias; the
212
210
  # lossy-map_err half still judged it.
213
211
  [[ -n "$crate" ]] || continue
214
- case " $checked_crates " in *" $crate "*) continue ;; esac
215
- checked_crates="$checked_crates $crate"
216
- if ! git ls-files --cached --others --exclude-standard -- "$crate/*.rs" \
217
- | xargs grep -lE "$RESULT_ALIAS_PATTERN" 2>/dev/null | grep -q .; then
212
+ already_checked=0
213
+ for checked in ${checked_crates[@]+"${checked_crates[@]}"}; do
214
+ [ "$checked" != "$crate" ] || already_checked=1
215
+ done
216
+ [ "$already_checked" -eq 0 ] || continue
217
+ checked_crates+=("$crate")
218
+ alias_found=0
219
+ while IFS= read -r -d '' alias_path; do
220
+ if grep -qE "$RESULT_ALIAS_PATTERN" "$alias_path"; then alias_found=1; break; fi
221
+ done < <(git ls-files --cached --others --exclude-standard -z -- "$crate/*.rs")
222
+ if [ "$alias_found" -eq 0 ]; then
218
223
  alias_count=$((alias_count + 1))
219
224
  fail "$path $RULE: crate '$crate' declares a public Error type with no \`pub type Result<T, E = Error>\` beside it — a reader must not have to check WHICH error a signature returns."
220
225
  fi
@@ -0,0 +1,88 @@
1
+ #!/usr/bin/env bash
2
+ # Shared filename scope and coherent index reads for deterministic scanners.
3
+
4
+ AUDIT_HOME="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
+
6
+ audit_scope_init() {
7
+ AUDIT_MODE="${SCOPE:-$1}"; shift
8
+ AUDIT_TARGETS=()
9
+ local arg selected=0 literal=0
10
+ for arg in "$@"; do
11
+ if [ "$literal" -eq 1 ]; then AUDIT_TARGETS+=("$arg"); continue; fi
12
+ case "$arg" in
13
+ --) literal=1 ;;
14
+ --staged|staged|--all|all)
15
+ [ "$selected" -eq 0 ] || { printf 'choose one audit mode\n' >&2; return 2; }
16
+ AUDIT_MODE="$arg"; selected=1 ;;
17
+ --strict)
18
+ [ "${0##*/}" = logging.sh ] || { printf 'unsupported audit option: %s\n' "$arg" >&2; return 2; } ;;
19
+ -h|--help) printf 'usage: %s [--all|--staged] | file [...]\n' "$0"; exit 0 ;;
20
+ -*) printf 'unknown audit option: %s\n' "$arg" >&2; return 2 ;;
21
+ *) AUDIT_TARGETS+=("$arg") ;;
22
+ esac
23
+ done
24
+ if [ "${#AUDIT_TARGETS[@]}" -gt 0 ]; then
25
+ [ "$selected" -eq 0 ] || { printf 'audit modes and explicit files cannot be mixed\n' >&2; return 2; }
26
+ AUDIT_MODE=explicit
27
+ fi
28
+ case "$AUDIT_MODE" in
29
+ staged|--staged) AUDIT_MODE=--staged ;;
30
+ all|--all) AUDIT_MODE=--all ;;
31
+ explicit) ;;
32
+ *) printf 'unknown audit scope: %s\n' "$AUDIT_MODE" >&2; return 2 ;;
33
+ esac
34
+ }
35
+
36
+ # Source and dependencies share a copied index. Git's object store is read-only;
37
+ # the index copy and checked-out files belong to this invocation alone.
38
+ audit_index_snapshot() {
39
+ [ "$AUDIT_MODE" = --staged ] || return 0
40
+ local gitdir index
41
+ AUDIT_SNAPSHOT_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/orly-index.XXXXXX")" || return 1
42
+ trap 'rm -rf -- "$AUDIT_SNAPSHOT_ROOT"' EXIT
43
+ trap 'exit 130' INT
44
+ trap 'exit 143' TERM
45
+ gitdir="$(git rev-parse --absolute-git-dir)" || exit 1
46
+ index="$(git rev-parse --git-path index)" || exit 1
47
+ cp "$index" "$AUDIT_SNAPSHOT_ROOT/index" || exit 1
48
+ export GIT_DIR="$gitdir" GIT_INDEX_FILE="$AUDIT_SNAPSHOT_ROOT/index" GIT_OPTIONAL_LOCKS=0
49
+ export GIT_WORK_TREE="$AUDIT_SNAPSHOT_ROOT/tree"
50
+ mkdir "$GIT_WORK_TREE" || exit 1
51
+ git checkout-index --all --prefix="$GIT_WORK_TREE/" || exit 1
52
+ cd "$GIT_WORK_TREE" || exit 1
53
+ [ -z "${TARGET_ROOT:-}" ] || TARGET_ROOT="$GIT_WORK_TREE"
54
+ audit_index_links || exit 2
55
+ }
56
+
57
+ audit_index_links() {
58
+ local entry path
59
+ git ls-files --stage -z > "$AUDIT_SNAPSHOT_ROOT/entries" || return 1
60
+ while IFS= read -r -d '' entry; do
61
+ case "$entry" in 120000*) ;; *) continue ;; esac
62
+ path="${entry#*$'\t'}"
63
+ bun -e 'import {realpathSync} from "node:fs";
64
+ import {relative, isAbsolute} from "node:path";
65
+ try {
66
+ const p = relative(process.cwd(), realpathSync(process.argv[1]));
67
+ if (p === ".." || p.startsWith("../") || isAbsolute(p)) throw new Error("escaping index link");
68
+ } catch { console.error("unsafe indexed symbolic link: " + process.argv[1]); process.exit(2); }' "$path" || return 2
69
+ done < "$AUDIT_SNAPSHOT_ROOT/entries"
70
+ }
71
+
72
+ # Zero-delimited throughout: spaces, tabs, quotes and newlines are filenames.
73
+ audit_scope_paths() {
74
+ local path pattern matched
75
+ if [ "$AUDIT_MODE" = explicit ]; then
76
+ for path in "${AUDIT_TARGETS[@]}"; do
77
+ matched=0
78
+ for pattern in "$@"; do
79
+ case "$path" in $pattern) matched=1 ;; esac
80
+ done
81
+ [ "$matched" -eq 0 ] || printf '%s\0' "$path"
82
+ done
83
+ elif [ "$AUDIT_MODE" = --staged ]; then
84
+ git diff --cached --name-only -z --diff-filter=ACMRT -- "$@"
85
+ else
86
+ git ls-files --cached --others --exclude-standard -z -- "$@"
87
+ fi
88
+ }
@@ -14,7 +14,7 @@
14
14
  # half that makes a spec "built for the agent".
15
15
  # 2b. DECLARED-COMMAND PARITY — a pending/active spec's Acceptance Rubric
16
16
  # must quote the repository's declared conform + applicable verify.* commands
17
- # (.oracle/orly.json) verbatim: the rubric and `orly gate` grade one
17
+ # (.orly/orly.json) verbatim: the rubric and `orly gate` grade one
18
18
  # boundary. Staged/file scope only; done/ specs stay historical.
19
19
  #
20
20
  # Dispatch façade: dispatch/write_spec.md (SPEC TEMPLATE GATE)
@@ -51,7 +51,7 @@ case "$MODE" in
51
51
  --staged|staged)
52
52
  # while read, not mapfile — bash-3.2 portability (see scripts/run-playbook-tests.sh).
53
53
  SPECS=()
54
- while IFS= read -r s; do SPECS+=("$s"); done < <(git diff --cached --name-only --diff-filter=ACMRT | grep -E '^docs/v[0-9]+/(pending|active|done)/.*\.md$' || true)
54
+ while IFS= read -r s; do SPECS+=("$s"); done < <(git diff --cached --name-only --diff-filter=ACMRT | grep -E '^docs/v[0-9]+(\.[0-9]+)*/(pending|active|done)/.*\.md$' || true)
55
55
  ;;
56
56
  --all|all)
57
57
  SPECS=()
@@ -6,7 +6,7 @@ type MarkdownNode = string | MarkdownElement;
6
6
  type Section = { title: string; nodes: MarkdownNode[] };
7
7
  type Commands = Record<string, string[][]>;
8
8
 
9
- const CONFIG_PATH = ".oracle/orly.json";
9
+ const CONFIG_PATH = ".orly/orly.json";
10
10
  const SPEC_LINE_LIMIT = 320;
11
11
  const BACKTICK = "`";
12
12
  const EMPTY = "";
package/audits/sql-mod.sh CHANGED
@@ -23,6 +23,10 @@
23
23
 
24
24
  set -euo pipefail
25
25
 
26
+ source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/scope.sh"
27
+ audit_scope_init --all "$@"
28
+ audit_index_snapshot "$@"
29
+
26
30
  MODE="${1:-${SCOPE:---all}}"
27
31
  ROOT="$(git rev-parse --show-toplevel)"
28
32
  cd "$ROOT"
@@ -44,20 +48,13 @@ added_hit() {
44
48
  | grep -nE "$DEF_RE|$BODY_RE" | head -1 || true
45
49
  }
46
50
 
47
- case "$MODE" in
48
- --staged|staged)
49
- # while read, not mapfile — bash-3.2 portability (see scripts/run-playbook-tests.sh).
50
- FILES=()
51
- while IFS= read -r f; do FILES+=("$f"); done < <(git diff --cached --name-only --diff-filter=ACMRT -- '*.zig' || true)
52
- STAGED=1 ;;
53
- --all|all)
54
- FILES=()
55
- while IFS= read -r f; do FILES+=("$f"); done < <(find src -type f -name '*.zig' 2>/dev/null || true)
56
- STAGED=0 ;;
57
- *)
58
- printf "usage: %s [--staged|--all]\n" "$0" >&2
59
- exit 64 ;;
60
- esac
51
+ MODE="$AUDIT_MODE"
52
+ FILES=()
53
+ while IFS= read -r -d '' f; do
54
+ [ ! -f "$f" ] || FILES+=("$f")
55
+ done < <(audit_scope_paths '*.zig')
56
+ STAGED=0
57
+ [ "$MODE" != --staged ] || STAGED=1
61
58
 
62
59
  if [[ ${#FILES[@]} -eq 0 ]]; then
63
60
  ok "no zig source files in scope ($MODE)"
@@ -0,0 +1,164 @@
1
+ # Extracted intact; the owning shell scanner supplies the filename scope.
2
+ FNR == 1 {
3
+ prev_file = FILENAME
4
+ # Language lane. Each lane below carves out what its syntax makes
5
+ # UNFIXABLE — a literal a named const cannot legally replace. Anything
6
+ # a const CAN replace stays in scope, in every language.
7
+ lang = "other"
8
+ if (FILENAME ~ /\.zig$/) lang = "zig"
9
+ else if (FILENAME ~ /\.rs$/) lang = "rust"
10
+ else if (FILENAME ~ /\.go$/) lang = "go"
11
+ # Test files: repetition is fixture data, not magic strings.
12
+ # Skip string-dup-file for tests; other checks still apply.
13
+ is_test = (FILENAME ~ /(_test\.zig|_test\.go|\.test\.|\.spec\.|\.unit\.test|\.integration\.test|\/test\/|\/tests\/)/)
14
+ # Directory-shaped test and benchmark trees. Rust puts both at the CRATE
15
+ # ROOT (`tests/`, `benches/`) with no leading slash, so the `\/tests\/`
16
+ # form above — which needs a parent directory — walked straight past them
17
+ # and audited 100 fixture hits as production literals.
18
+ if (FILENAME ~ "(^|/)(test|tests|benches|__tests__)/") is_test = 1
19
+ }
20
+ is_test { next }
21
+ # ui/ files: extracting class-strings or short literals to file-local
22
+ # consts is fragile in TypeScript (type-position uses widen string|
23
+ # literal types, and the cleanup belongs in a UI-aware spec). Skip
24
+ # string-dup-file for ui/packages/*/src; cross-runtime-orphan still
25
+ # runs against ui/ to catch ERR_* parity drift.
26
+ # macOS/BSD awk aborts on a `/` inside a `[...]` class in a regex *literal*
27
+ # ("nonterminated character class"); use a dynamic-regex STRING — identical
28
+ # match on gawk, portable on BWK awk (the macOS default). Do not re-inline.
29
+ FILENAME ~ "^ui/packages/[^/]+/(src|app|tests|components|lib|hooks)/" { next }
30
+ FNR == 1 { in_test_block = 0; test_depth = 0; in_block_comment = 0; in_rs_test = 0; rs_open = 0; rs_depth = 0; in_go_const = 0 }
31
+ {
32
+ line = $0
33
+ # Block-comment exclusion (TS/JS/Zig non-applicable): skip lines inside /* ... */
34
+ if (in_block_comment) {
35
+ if (line ~ /\*\//) in_block_comment = 0
36
+ next
37
+ }
38
+ if (line ~ /\/\*/ && line !~ /\*\//) {
39
+ in_block_comment = 1
40
+ next
41
+ }
42
+ # Zig multi-line string literal — lines start with `\\` after whitespace.
43
+ if (lang == "zig" && line ~ /^[[:space:]]*\\\\/) next
44
+ # Inline-test exclusion (Zig): track depth across `test "..." {` blocks.
45
+ if (lang == "zig") {
46
+ if (in_test_block) {
47
+ test_depth += gsub(/\{/, "{", line) - gsub(/\}/, "}", line)
48
+ if (test_depth <= 0) in_test_block = 0
49
+ line = $0 # restore
50
+ next
51
+ }
52
+ if (line ~ /^test[[:space:]]+"/) {
53
+ in_test_block = 1
54
+ tmp = line
55
+ test_depth = gsub(/\{/, "{", tmp) - gsub(/\}/, "}", tmp)
56
+ if (test_depth <= 0) in_test_block = 0
57
+ next
58
+ }
59
+ }
60
+ # Inline-test exclusion (Rust): Rust keeps its unit tests INSIDE the file
61
+ # they cover, under `#[cfg(test)] mod tests { ... }`, so without this the
62
+ # fixture keys of a well-tested module read as the worst literal debt in
63
+ # the production half of that same file — a real crate reported `"key1"`
64
+ # 15 times from one such block. Same brace-depth shape as the Zig branch.
65
+ if (lang == "rust") {
66
+ if (in_rs_test) {
67
+ tmp = line
68
+ rs_depth += gsub(/\{/, "{", tmp) - gsub(/\}/, "}", tmp)
69
+ if (!rs_open && index(line, "{") > 0) rs_open = 1
70
+ if (rs_open && rs_depth <= 0) in_rs_test = 0
71
+ else if (!rs_open && line ~ /;[[:space:]]*$/) in_rs_test = 0
72
+ next
73
+ }
74
+ # Three ways Rust marks a block as fixture data, not shipped behaviour:
75
+ # `#[cfg(test)]`, a `#[test]`/`#[tokio::test]` function, and a
76
+ # FEATURE-GATED test seam — `#[cfg(feature = "test-util")]`, the
77
+ # convention for a helper that must compile into the crate so an
78
+ # integration test in a sibling file can call it (a `one_of_each_kind`
79
+ # error-surface enumerator is the usual shape). The feature name is
80
+ # matched narrowly, `test` as a whole word or a `-`/`_` segment, so a
81
+ # production gate like `#[cfg(feature = "redis")]` still counts and
82
+ # `"latest"` is not mistaken for a test seam.
83
+ if (line ~ /^[[:space:]]*#\[cfg\(test\)\]/ ||
84
+ line ~ /^[[:space:]]*#\[[A-Za-z_:]*test\]/ ||
85
+ line ~ /^[[:space:]]*#\[cfg\(.*feature[[:space:]]*=[[:space:]]*"([A-Za-z0-9]+[-_])?test(ing)?([-_][A-Za-z0-9_-]+)?"/) {
86
+ in_rs_test = 1; rs_open = 0; rs_depth = 0
87
+ next
88
+ }
89
+ # Attribute literals are UNFIXABLE, not undisciplined. `#[serde(rename =
90
+ # "...")]`, `#[cfg(feature = "...")]` and `#[doc = "..."]` take a literal
91
+ # token by language rule — a const is not accepted there, so naming one
92
+ # cannot resolve the hit and reporting it only teaches people to ignore
93
+ # the gate. Ordered after the cfg(test) branch, which is also a `#[`.
94
+ if (line ~ /^[[:space:]]*#!?\[/) next
95
+ }
96
+ # Grouped-const exclusion (Go): `const ( ... )` states the binding keyword
97
+ # ONCE, on the opening line, so a member like `metaKeyQoS = "conf_..."` is
98
+ # a definition carrying no `const` for the binding carve-out below to see.
99
+ # `var (` groups are deliberately NOT tracked — the carve-out is const-only
100
+ # in every other language and widening it here would be a silent divergence.
101
+ if (lang == "go") {
102
+ if (in_go_const) {
103
+ if (line ~ /^[[:space:]]*\)/) { in_go_const = 0; next }
104
+ if (line ~ /^[[:space:]]*[A-Za-z_][A-Za-z0-9_]*([[:space:]]+[A-Za-z0-9_.\[\]*]+)?[[:space:]]*=/) next
105
+ }
106
+ else if (line ~ /^[[:space:]]*const[[:space:]]*\([[:space:]]*$/) { in_go_const = 1; next }
107
+ }
108
+ sub(/\/\/.*$/, "", line)
109
+ # Strip single-line /* ... */ inline block comments (jsdoc/etc) so
110
+ # literals inside them are not counted.
111
+ gsub(/\/\*[^*]*\*+([^\/*][^*]*\*+)*\//, "", line)
112
+ # Const-BINDING carve-out (narrower than the numeric-suspect is_const_decl
113
+ # exemption below — deliberately): a literal that IS the right-hand side of a
114
+ # `const`/`pub const`/`export const` binding is its single-source DEFINITION,
115
+ # not a magic-string use. Two distinct named constants may legitimately share
116
+ # a value across domains (e.g. a runner status and a lease status both
117
+ # "active") — RULE UFS targets UN-named repetition, and both sites there are
118
+ # already named. But a literal passed as a CALL ARGUMENT on a const line
119
+ # (`const x = foo("lit");`) is NOT named by that const — it counts and flags
120
+ # like any other use. (A prior line-level skip exempted those too and silently
121
+ # gutted the check for most Zig code; the ufs_dup_string eval fixture pins the
122
+ # binding-level semantic.)
123
+ # `static` joins the keyword set for Rust: `static NAME: &str = "..."` binds
124
+ # a literal to a name exactly as `const` does, and the leading
125
+ # `(^|[[:space:]])` anchor already carries the `pub`/`pub(crate)` prefixes.
126
+ # The second type slot belongs to Go, which spells the type with no colon
127
+ # (`const name string = "..."`), which the colon-only slot could not see.
128
+ if (line ~ /(^|[[:space:]])(pub[[:space:]]+const|export[[:space:]]+const|const|static)[[:space:]]+[A-Za-z_$][A-Za-z0-9_$]*([[:space:]]*:[^=]*|[[:space:]]+[A-Za-z_][A-Za-z0-9_.\[\]*]*)?[[:space:]]*=[[:space:]]*"((\\.)|[^\\"])+"[[:space:]]*(as[[:space:]]+const)?[[:space:]]*[;,]?[[:space:]]*$/) next
129
+ rest = line
130
+ # Strip Zig identifier-escape syntax @"name" — body is an identifier,
131
+ # not a string literal, but the regex below would otherwise match it.
132
+ gsub(/@"[^"]*"/, "", rest)
133
+ # Strip Go backtick runs. They are overwhelmingly struct tags
134
+ # (`json:"id" yaml:"id"`), whose quoted halves are tag SYNTAX addressed by
135
+ # reflection — a const cannot appear inside one, so the repeat is unfixable.
136
+ # This also drops single-line Go raw strings, which is the accepted cost:
137
+ # the alternative reports every `json:"id"` in a wire struct as a violation.
138
+ # A backtick run spanning lines is not tracked (best-effort, as documented).
139
+ if (lang == "go") gsub(/`[^`]*`/, "", rest)
140
+ # Strip empty string literals so the regex below cannot fuse
141
+ # two adjacent Zig literals through their inner gap.
142
+ gsub(/""/, "", rest)
143
+ # Match a quoted string literal honouring \" escapes: open quote,
144
+ # then runs of (backslash+any-char) | (non-backslash-non-quote),
145
+ # then close quote. Min 3 chars total ensures literal body ≥1 char
146
+ # (back-compat with the {2,} body-min from the prior regex).
147
+ while (match(rest, /"((\\.)|[^\\"])+"/)) {
148
+ lit = substr(rest, RSTART, RLENGTH)
149
+ rest = substr(rest, RSTART + RLENGTH)
150
+ if (length(lit) < 4) continue
151
+ if (lit ~ /^"(http|https|file|\/|\\\\|\\\\n)/) continue
152
+ key = FILENAME "\034" lit
153
+ count[key]++
154
+ file_of[key] = FILENAME
155
+ lit_of[key] = lit
156
+ }
157
+ }
158
+ END {
159
+ for (k in count) {
160
+ if (count[k] >= 2) {
161
+ printf "string-dup-file %s %s %d\n", file_of[k], lit_of[k], count[k]
162
+ }
163
+ }
164
+ }
package/audits/ufs.sh CHANGED
@@ -53,20 +53,15 @@
53
53
 
54
54
  set -euo pipefail
55
55
 
56
+ source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/scope.sh"
57
+ audit_scope_init --all "$@"
58
+ audit_index_snapshot "$@"
59
+
56
60
  # Default: full-codebase scan (`--all` is an explicit alias). `--staged` is the
57
61
  # pre-commit lens — it narrows the per-file checks to `git diff --cached`. The
58
62
  # retired `--diff` (BASE...HEAD) mode stays rejected; `--staged` reads the index
59
63
  # and so is not blind to staged-but-uncommitted fixes (M70's concern).
60
- MODE="${1:-}"
61
- case "$MODE" in
62
- ""|--all|all) MODE="--all" ;;
63
- --staged|staged) MODE="--staged" ;;
64
- *)
65
- printf "usage: %s [--all|--staged]\n" "$0" >&2
66
- 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
67
- exit 2
68
- ;;
69
- esac
64
+ MODE="$AUDIT_MODE"
70
65
  ROOT="$(git rev-parse --show-toplevel)"
71
66
  cd "$ROOT"
72
67
 
@@ -111,19 +106,10 @@ is_source() {
111
106
 
112
107
  # Per-file check scope (string-dup-file, numeric-suspect). --staged narrows to
113
108
  # the commit; cross-runtime-orphan (below) always scans the full tree.
114
- if [ "$MODE" = "--staged" ]; then
115
- scope_files() { git diff --cached --name-only --diff-filter=ACMRT; }
116
- else
117
- scope_files() { git ls-files; }
118
- fi
119
- # `while read` rather than mapfile: mapfile is bash 4+ and macOS ships 3.2 —
120
- # the portability rule scripts/run-playbook-tests.sh already records.
121
109
  FILES=()
122
- while IFS= read -r f; do
123
- FILES+=("$f")
124
- done < <(scope_files | while read -r f; do
125
- is_source "$f" && echo "$f"
126
- done)
110
+ while IFS= read -r -d '' f; do
111
+ if is_source "$f" && [ -f "$f" ]; then FILES+=("$f"); fi
112
+ done < <(audit_scope_paths '*.zig' '*.ts' '*.tsx' '*.js' '*.jsx' '*.rs' '*.go')
127
113
 
128
114
  [ "${#FILES[@]}" -eq 0 ] && { ok "audit-ufs: no source files in scope"; exit 0; }
129
115
 
@@ -142,171 +128,7 @@ done)
142
128
  # a subshell and silently dropped violations.
143
129
  while IFS= read -r row; do
144
130
  record "$row"
145
- done < <(awk '
146
- FNR == 1 {
147
- prev_file = FILENAME
148
- # Language lane. Each lane below carves out what its syntax makes
149
- # UNFIXABLE — a literal a named const cannot legally replace. Anything
150
- # a const CAN replace stays in scope, in every language.
151
- lang = "other"
152
- if (FILENAME ~ /\.zig$/) lang = "zig"
153
- else if (FILENAME ~ /\.rs$/) lang = "rust"
154
- else if (FILENAME ~ /\.go$/) lang = "go"
155
- # Test files: repetition is fixture data, not magic strings.
156
- # Skip string-dup-file for tests; other checks still apply.
157
- is_test = (FILENAME ~ /(_test\.zig|_test\.go|\.test\.|\.spec\.|\.unit\.test|\.integration\.test|\/test\/|\/tests\/)/)
158
- # Directory-shaped test and benchmark trees. Rust puts both at the CRATE
159
- # ROOT (`tests/`, `benches/`) with no leading slash, so the `\/tests\/`
160
- # form above — which needs a parent directory — walked straight past them
161
- # and audited 100 fixture hits as production literals.
162
- if (FILENAME ~ "(^|/)(test|tests|benches|__tests__)/") is_test = 1
163
- }
164
- is_test { next }
165
- # ui/ files: extracting class-strings or short literals to file-local
166
- # consts is fragile in TypeScript (type-position uses widen string|
167
- # literal types, and the cleanup belongs in a UI-aware spec). Skip
168
- # string-dup-file for ui/packages/*/src; cross-runtime-orphan still
169
- # runs against ui/ to catch ERR_* parity drift.
170
- # macOS/BSD awk aborts on a `/` inside a `[...]` class in a regex *literal*
171
- # ("nonterminated character class"); use a dynamic-regex STRING — identical
172
- # match on gawk, portable on BWK awk (the macOS default). Do not re-inline.
173
- FILENAME ~ "^ui/packages/[^/]+/(src|app|tests|components|lib|hooks)/" { next }
174
- FNR == 1 { in_test_block = 0; test_depth = 0; in_block_comment = 0; in_rs_test = 0; rs_open = 0; rs_depth = 0; in_go_const = 0 }
175
- {
176
- line = $0
177
- # Block-comment exclusion (TS/JS/Zig non-applicable): skip lines inside /* ... */
178
- if (in_block_comment) {
179
- if (line ~ /\*\//) in_block_comment = 0
180
- next
181
- }
182
- if (line ~ /\/\*/ && line !~ /\*\//) {
183
- in_block_comment = 1
184
- next
185
- }
186
- # Zig multi-line string literal — lines start with `\\` after whitespace.
187
- if (lang == "zig" && line ~ /^[[:space:]]*\\\\/) next
188
- # Inline-test exclusion (Zig): track depth across `test "..." {` blocks.
189
- if (lang == "zig") {
190
- if (in_test_block) {
191
- test_depth += gsub(/\{/, "{", line) - gsub(/\}/, "}", line)
192
- if (test_depth <= 0) in_test_block = 0
193
- line = $0 # restore
194
- next
195
- }
196
- if (line ~ /^test[[:space:]]+"/) {
197
- in_test_block = 1
198
- tmp = line
199
- test_depth = gsub(/\{/, "{", tmp) - gsub(/\}/, "}", tmp)
200
- if (test_depth <= 0) in_test_block = 0
201
- next
202
- }
203
- }
204
- # Inline-test exclusion (Rust): Rust keeps its unit tests INSIDE the file
205
- # they cover, under `#[cfg(test)] mod tests { ... }`, so without this the
206
- # fixture keys of a well-tested module read as the worst literal debt in
207
- # the production half of that same file — a real crate reported `"key1"`
208
- # 15 times from one such block. Same brace-depth shape as the Zig branch.
209
- if (lang == "rust") {
210
- if (in_rs_test) {
211
- tmp = line
212
- rs_depth += gsub(/\{/, "{", tmp) - gsub(/\}/, "}", tmp)
213
- if (!rs_open && index(line, "{") > 0) rs_open = 1
214
- if (rs_open && rs_depth <= 0) in_rs_test = 0
215
- else if (!rs_open && line ~ /;[[:space:]]*$/) in_rs_test = 0
216
- next
217
- }
218
- # Three ways Rust marks a block as fixture data, not shipped behaviour:
219
- # `#[cfg(test)]`, a `#[test]`/`#[tokio::test]` function, and a
220
- # FEATURE-GATED test seam — `#[cfg(feature = "test-util")]`, the
221
- # convention for a helper that must compile into the crate so an
222
- # integration test in a sibling file can call it (a `one_of_each_kind`
223
- # error-surface enumerator is the usual shape). The feature name is
224
- # matched narrowly, `test` as a whole word or a `-`/`_` segment, so a
225
- # production gate like `#[cfg(feature = "redis")]` still counts and
226
- # `"latest"` is not mistaken for a test seam.
227
- if (line ~ /^[[:space:]]*#\[cfg\(test\)\]/ ||
228
- line ~ /^[[:space:]]*#\[[A-Za-z_:]*test\]/ ||
229
- line ~ /^[[:space:]]*#\[cfg\(.*feature[[:space:]]*=[[:space:]]*"([A-Za-z0-9]+[-_])?test(ing)?([-_][A-Za-z0-9_-]+)?"/) {
230
- in_rs_test = 1; rs_open = 0; rs_depth = 0
231
- next
232
- }
233
- # Attribute literals are UNFIXABLE, not undisciplined. `#[serde(rename =
234
- # "...")]`, `#[cfg(feature = "...")]` and `#[doc = "..."]` take a literal
235
- # token by language rule — a const is not accepted there, so naming one
236
- # cannot resolve the hit and reporting it only teaches people to ignore
237
- # the gate. Ordered after the cfg(test) branch, which is also a `#[`.
238
- if (line ~ /^[[:space:]]*#!?\[/) next
239
- }
240
- # Grouped-const exclusion (Go): `const ( ... )` states the binding keyword
241
- # ONCE, on the opening line, so a member like `metaKeyQoS = "conf_..."` is
242
- # a definition carrying no `const` for the binding carve-out below to see.
243
- # `var (` groups are deliberately NOT tracked — the carve-out is const-only
244
- # in every other language and widening it here would be a silent divergence.
245
- if (lang == "go") {
246
- if (in_go_const) {
247
- if (line ~ /^[[:space:]]*\)/) { in_go_const = 0; next }
248
- if (line ~ /^[[:space:]]*[A-Za-z_][A-Za-z0-9_]*([[:space:]]+[A-Za-z0-9_.\[\]*]+)?[[:space:]]*=/) next
249
- }
250
- else if (line ~ /^[[:space:]]*const[[:space:]]*\([[:space:]]*$/) { in_go_const = 1; next }
251
- }
252
- sub(/\/\/.*$/, "", line)
253
- # Strip single-line /* ... */ inline block comments (jsdoc/etc) so
254
- # literals inside them are not counted.
255
- gsub(/\/\*[^*]*\*+([^\/*][^*]*\*+)*\//, "", line)
256
- # Const-BINDING carve-out (narrower than the numeric-suspect is_const_decl
257
- # exemption below — deliberately): a literal that IS the right-hand side of a
258
- # `const`/`pub const`/`export const` binding is its single-source DEFINITION,
259
- # not a magic-string use. Two distinct named constants may legitimately share
260
- # a value across domains (e.g. a runner status and a lease status both
261
- # "active") — RULE UFS targets UN-named repetition, and both sites there are
262
- # already named. But a literal passed as a CALL ARGUMENT on a const line
263
- # (`const x = foo("lit");`) is NOT named by that const — it counts and flags
264
- # like any other use. (A prior line-level skip exempted those too and silently
265
- # gutted the check for most Zig code; the ufs_dup_string eval fixture pins the
266
- # binding-level semantic.)
267
- # `static` joins the keyword set for Rust: `static NAME: &str = "..."` binds
268
- # a literal to a name exactly as `const` does, and the leading
269
- # `(^|[[:space:]])` anchor already carries the `pub`/`pub(crate)` prefixes.
270
- # The second type slot belongs to Go, which spells the type with no colon
271
- # (`const name string = "..."`), which the colon-only slot could not see.
272
- if (line ~ /(^|[[:space:]])(pub[[:space:]]+const|export[[:space:]]+const|const|static)[[:space:]]+[A-Za-z_$][A-Za-z0-9_$]*([[:space:]]*:[^=]*|[[:space:]]+[A-Za-z_][A-Za-z0-9_.\[\]*]*)?[[:space:]]*=[[:space:]]*"((\\.)|[^\\"])+"[[:space:]]*(as[[:space:]]+const)?[[:space:]]*[;,]?[[:space:]]*$/) next
273
- rest = line
274
- # Strip Zig identifier-escape syntax @"name" — body is an identifier,
275
- # not a string literal, but the regex below would otherwise match it.
276
- gsub(/@"[^"]*"/, "", rest)
277
- # Strip Go backtick runs. They are overwhelmingly struct tags
278
- # (`json:"id" yaml:"id"`), whose quoted halves are tag SYNTAX addressed by
279
- # reflection — a const cannot appear inside one, so the repeat is unfixable.
280
- # This also drops single-line Go raw strings, which is the accepted cost:
281
- # the alternative reports every `json:"id"` in a wire struct as a violation.
282
- # A backtick run spanning lines is not tracked (best-effort, as documented).
283
- if (lang == "go") gsub(/`[^`]*`/, "", rest)
284
- # Strip empty string literals so the regex below cannot fuse
285
- # two adjacent Zig literals through their inner gap.
286
- gsub(/""/, "", rest)
287
- # Match a quoted string literal honouring \" escapes: open quote,
288
- # then runs of (backslash+any-char) | (non-backslash-non-quote),
289
- # then close quote. Min 3 chars total ensures literal body ≥1 char
290
- # (back-compat with the {2,} body-min from the prior regex).
291
- while (match(rest, /"((\\.)|[^\\"])+"/)) {
292
- lit = substr(rest, RSTART, RLENGTH)
293
- rest = substr(rest, RSTART + RLENGTH)
294
- if (length(lit) < 4) continue
295
- if (lit ~ /^"(http|https|file|\/|\\\\|\\\\n)/) continue
296
- key = FILENAME "\034" lit
297
- count[key]++
298
- file_of[key] = FILENAME
299
- lit_of[key] = lit
300
- }
301
- }
302
- END {
303
- for (k in count) {
304
- if (count[k] >= 2) {
305
- printf "string-dup-file %s %s %d\n", file_of[k], lit_of[k], count[k]
306
- }
307
- }
308
- }
309
- ' "${FILES[@]}")
131
+ done < <(awk -f "$AUDIT_HOME/ufs-strings.awk" "${FILES[@]}")
310
132
 
311
133
  # ── 2. numeric-suspect ──────────────────────────────────────────────────────
312
134
  # Flag bare power-of-ten and unit-factor numerics in expressions.
@@ -321,7 +143,7 @@ done < <(awk '
321
143
  # Boundaries are capturing groups, not \b: macOS/BSD awk treats \b as a backspace,
322
144
  # so word-boundary anchors silently match nothing. The leading/trailing boundary
323
145
  # chars are trimmed back off the extracted token below (see RSTART/RLENGTH use).
324
- 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_]|$)'
146
+ NUMERIC_RE='(^|[^0-9A-Za-z_])(1[_e]?0{3,12}|1_000(_000)*|10_000_000|100_000|1024|1048576|60|3600|86400)([^0-9A-Za-z_]|$)'
325
147
 
326
148
  # Perf (M70): single awk across all files; FNR == 1 detects file boundary
327
149
  # so the "prev line carve-out" stays per-file. Process substitution keeps