@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,51 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# parity-dispatch.sh — the Stage-2 replacement for agents-md.sh's gate parity.
|
|
3
|
+
#
|
|
4
|
+
# Model B dissolves docs/gates/ (20 cards) into dispatch/ (10 entries). At the
|
|
5
|
+
# Stage-2 switchover, agents-md.sh's gate-parity block (`index ↔ disk ↔
|
|
6
|
+
# REQUIRED_GATES`, over docs/gates/) is deleted and this is sourced + called in
|
|
7
|
+
# its place — line-neutral, so the ≤350 length gate on agents-md.sh still holds.
|
|
8
|
+
#
|
|
9
|
+
# Until then it is exercised only by evals/test-dispatch-parity.sh against a
|
|
10
|
+
# model-B sandbox, proving the switchover audit goes GREEN and BITES before any
|
|
11
|
+
# card is deleted. It does not run on the live (gate-mode) tree.
|
|
12
|
+
#
|
|
13
|
+
# check_dispatch_parity <ROOT> # echoes PASS/FAIL lines, returns 0/1
|
|
14
|
+
#
|
|
15
|
+
# Sources of truth (must agree):
|
|
16
|
+
# (a) disk — dispatch/*.md (each entry has exactly one latent .md)
|
|
17
|
+
# (b) index — AGENTS.md dispatch-table rows (2nd cell a backticked entry)
|
|
18
|
+
# (c) expected — REQUIRED_DISPATCH (audits/data.sh)
|
|
19
|
+
|
|
20
|
+
check_dispatch_parity() {
|
|
21
|
+
local root="$1" rc=0 e
|
|
22
|
+
local agents="$root/AGENTS.md" # separate line: $root must be set before it expands here
|
|
23
|
+
local bt='`' # backtick held in a var (literal ` is fine, but keep it explicit)
|
|
24
|
+
|
|
25
|
+
local disk_count table_rows required_count
|
|
26
|
+
disk_count=$(find "$root/dispatch" -maxdepth 1 -name '*.md' 2>/dev/null | wc -l | tr -d ' ')
|
|
27
|
+
table_rows=$(grep -cE "^\|[^|]*\| *${bt}[a-z_]+${bt} *\|" "$agents" 2>/dev/null)
|
|
28
|
+
required_count=${#REQUIRED_DISPATCH[@]}
|
|
29
|
+
|
|
30
|
+
if [[ "$disk_count" -ne "$table_rows" || "$table_rows" -ne "$required_count" ]]; then
|
|
31
|
+
echo "FAIL: dispatch parity — disk=$disk_count ↔ table=$table_rows ↔ REQUIRED=$required_count (all three must match)"
|
|
32
|
+
rc=1
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
for e in "${REQUIRED_DISPATCH[@]}"; do
|
|
36
|
+
[[ -f "$root/dispatch/$e.md" ]] \
|
|
37
|
+
|| { echo "FAIL: dispatch entry has no body: dispatch/$e.md"; rc=1; }
|
|
38
|
+
grep -qE "^\|[^|]*\| *${bt}$e${bt} *\|" "$agents" 2>/dev/null \
|
|
39
|
+
|| { echo "FAIL: dispatch entry absent from AGENTS.md table: $e"; rc=1; }
|
|
40
|
+
done
|
|
41
|
+
|
|
42
|
+
# docs/gates/ must be EMPTY in the dispatch model (the old empty-set guard
|
|
43
|
+
# inverts: a leftover card means the switchover is half-done).
|
|
44
|
+
local leftover
|
|
45
|
+
leftover=$(find "$root/docs/gates" -maxdepth 1 -name '*.md' 2>/dev/null | wc -l | tr -d ' ')
|
|
46
|
+
[[ "$leftover" -eq 0 ]] \
|
|
47
|
+
|| { echo "FAIL: docs/gates/ still holds $leftover card(s) — switchover incomplete"; rc=1; }
|
|
48
|
+
|
|
49
|
+
[[ $rc -eq 0 ]] && echo "PASS: dispatch parity (disk=$disk_count ↔ table=$table_rows ↔ REQUIRED=$required_count; docs/gates/ empty)"
|
|
50
|
+
return $rc
|
|
51
|
+
}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# rule-ledger-lib.sh — shared extractors for audits/rule-ledger.sh.
|
|
3
|
+
#
|
|
4
|
+
# Sourced, never executed. Split from the leaf per dispatch/write_any.md
|
|
5
|
+
# §File & Function Length Gate so both stay well under the cap.
|
|
6
|
+
#
|
|
7
|
+
# The docs/ rule tier is the half of the corpus that dispatch/'s coverage audit
|
|
8
|
+
# cannot see: evals/dispatch/coverage.sh globs dispatch/*.md only. This library
|
|
9
|
+
# supplies the same kind of extraction for docs/, plus the UNENFORCED class that
|
|
10
|
+
# makes "prose nobody checks" a counted number instead of an unknown.
|
|
11
|
+
|
|
12
|
+
# Rule documents whose enforcement this ledger reports. A docs/*.md cited as a
|
|
13
|
+
# rule read by a dispatch façade but absent from BOTH lists is a parity red:
|
|
14
|
+
# either it belongs here, or it is not a rule doc and belongs in EXCLUDED.
|
|
15
|
+
REGISTERED_DOCS=(
|
|
16
|
+
"docs/LOGGING_STANDARD.md"
|
|
17
|
+
"docs/REST_API_DESIGN_GUIDELINES.md"
|
|
18
|
+
"docs/SCHEMA_CONVENTIONS.md"
|
|
19
|
+
"docs/DOCUMENTATION_RULES.md"
|
|
20
|
+
"docs/LIFECYCLE_PATTERNS.md"
|
|
21
|
+
"docs/CHANGELOG_VOICE.md"
|
|
22
|
+
"docs/VERIFY_TIERS.md"
|
|
23
|
+
"docs/greptile-learnings/RULES.md"
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
# Cited by a façade but deliberately not a rule doc: architecture (describes
|
|
27
|
+
# shape, enforces nothing), the spec template (a form, gated by
|
|
28
|
+
# audits/spec-template.sh), the trigger index, an output-format reference, and
|
|
29
|
+
# product-repo docs that do not exist in this checkout at all.
|
|
30
|
+
EXCLUDED_DOCS=(
|
|
31
|
+
"docs/DISPATCH_ARCHITECTURE.md"
|
|
32
|
+
"docs/ORLY_ARCHITECTURE.md"
|
|
33
|
+
"docs/TEMPLATE.md"
|
|
34
|
+
"docs/EXECUTE_DOC_READS.md"
|
|
35
|
+
"docs/HARNESS_VERIFY_OUTPUT.md"
|
|
36
|
+
"docs/AUTH.md"
|
|
37
|
+
"docs/changelog.md"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# Prefix-matched exclusions for open-ended trees owned by other repositories.
|
|
41
|
+
EXCLUDED_PREFIXES=(
|
|
42
|
+
"docs/architecture/"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# A normative clause is a line that binds behaviour. This is a keyword
|
|
46
|
+
# heuristic, not a parse: counts INFORM the reader and never gate a build, so a
|
|
47
|
+
# keyword inside an example block costs nothing but a slightly high number.
|
|
48
|
+
NORMATIVE_PATTERN='\b(MUST|NEVER|ALWAYS|Forbidden|Required|SHALL|Do not|Don.t)\b'
|
|
49
|
+
|
|
50
|
+
# Markdown table rows are excluded from the clause count. In a rule doc a table
|
|
51
|
+
# is usually a field reference — `| ts_ms | u64 | always | ... |` — where the
|
|
52
|
+
# keyword is a column VALUE, not an obligation, and counting those makes a
|
|
53
|
+
# fully-triaged document look permanently unfinished. Rules stated inside a
|
|
54
|
+
# table restate prose stated elsewhere; they are still read, just not counted.
|
|
55
|
+
TABLE_ROW_PATTERN='^[[:space:]]*\|'
|
|
56
|
+
|
|
57
|
+
# Enforcement classes. The first two already exist in dispatch/*.md
|
|
58
|
+
# (docs/DISPATCH_ARCHITECTURE.md §6); UNENFORCED is added by this milestone so
|
|
59
|
+
# an acknowledged-prose clause is distinguishable from one nobody has triaged.
|
|
60
|
+
DETERMINISTIC_TAG='\[DETERMINISTIC → [A-Za-z0-9_:-]+\]'
|
|
61
|
+
JUDGMENT_TAG='\[JUDGMENT → [A-Za-z0-9_:-]+\]'
|
|
62
|
+
UNENFORCED_TAG='\[UNENFORCED → [^]]+\]'
|
|
63
|
+
|
|
64
|
+
LEDGER_ROOT="${ORLY_ROOT:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
|
|
65
|
+
|
|
66
|
+
# A tag alone on its line (the `> [DETERMINISTIC → FLL]` form dispatch/*.md
|
|
67
|
+
# already uses) covers every clause under it until the next heading. A tag at
|
|
68
|
+
# the end of a sentence covers that sentence only. Both are needed: a rule doc
|
|
69
|
+
# states some rules as a tagged section and others as one-line asides, and
|
|
70
|
+
# forcing either shape onto the other would mean editing prose to satisfy a
|
|
71
|
+
# counter.
|
|
72
|
+
STANDALONE_TAG_PATTERN='^[[:space:]]*>?[[:space:]]*\[(DETERMINISTIC|JUDGMENT|UNENFORCED) → [^]]+\][[:space:]]*$'
|
|
73
|
+
HEADING_PATTERN='^#{1,6} '
|
|
74
|
+
CLASS_DETERMINISTIC="det"
|
|
75
|
+
CLASS_JUDGMENT="judgment"
|
|
76
|
+
CLASS_UNENFORCED="unenforced"
|
|
77
|
+
|
|
78
|
+
# Line numbers of every line in a file matching a pattern, one per line.
|
|
79
|
+
ledger_line_numbers() {
|
|
80
|
+
grep -nE "$2" "$1" 2>/dev/null | cut -d: -f1
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
# Clause line numbers: keyword lines that are not table rows. Case-insensitive,
|
|
84
|
+
# because a rule reads the same whether the author shouted it or not.
|
|
85
|
+
ledger_clause_lines() {
|
|
86
|
+
grep -niE "$NORMATIVE_PATTERN" "$1" 2>/dev/null \
|
|
87
|
+
| grep -vE '^[0-9]+:[[:space:]]*\|' | cut -d: -f1
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
# The five census numbers for one document, space-separated:
|
|
91
|
+
# clauses det judgment unenforced untagged
|
|
92
|
+
# Returns 1 without output when the path is absent, so a caller reports the
|
|
93
|
+
# missing registration rather than printing zeroes that look like a clean doc.
|
|
94
|
+
#
|
|
95
|
+
# One pass down the file carrying the section's block tag: a heading clears it,
|
|
96
|
+
# a standalone tag sets it, and every clause takes the tag on its own line
|
|
97
|
+
# first, the block's second, nothing third. Nothing third is the backlog.
|
|
98
|
+
ledger_doc_counts() {
|
|
99
|
+
local path="$1"
|
|
100
|
+
local line total block="" class
|
|
101
|
+
local clauses=0 det=0 judgment=0 unenforced=0 untagged=0
|
|
102
|
+
[ -f "$path" ] || return 1
|
|
103
|
+
local is_heading=() is_clause=() line_class=() standalone=()
|
|
104
|
+
for line in $(ledger_line_numbers "$path" "$HEADING_PATTERN"); do is_heading[$line]=1; done
|
|
105
|
+
for line in $(ledger_clause_lines "$path"); do is_clause[$line]=1; done
|
|
106
|
+
for line in $(ledger_line_numbers "$path" "$STANDALONE_TAG_PATTERN"); do standalone[$line]=1; done
|
|
107
|
+
for line in $(ledger_line_numbers "$path" "$DETERMINISTIC_TAG"); do line_class[$line]="$CLASS_DETERMINISTIC"; done
|
|
108
|
+
for line in $(ledger_line_numbers "$path" "$JUDGMENT_TAG"); do line_class[$line]="$CLASS_JUDGMENT"; done
|
|
109
|
+
for line in $(ledger_line_numbers "$path" "$UNENFORCED_TAG"); do line_class[$line]="$CLASS_UNENFORCED"; done
|
|
110
|
+
|
|
111
|
+
total="$(wc -l < "$path" | tr -d ' ')"
|
|
112
|
+
for (( line = 1; line <= total; line++ )); do
|
|
113
|
+
[ -n "${is_heading[$line]:-}" ] && { block=""; continue; }
|
|
114
|
+
[ -n "${standalone[$line]:-}" ] && { block="${line_class[$line]:-}"; continue; }
|
|
115
|
+
[ -n "${is_clause[$line]:-}" ] || continue
|
|
116
|
+
class="${line_class[$line]:-$block}"
|
|
117
|
+
clauses=$(( clauses + 1 ))
|
|
118
|
+
case "$class" in
|
|
119
|
+
"$CLASS_DETERMINISTIC") det=$(( det + 1 )) ;;
|
|
120
|
+
"$CLASS_JUDGMENT") judgment=$(( judgment + 1 )) ;;
|
|
121
|
+
"$CLASS_UNENFORCED") unenforced=$(( unenforced + 1 )) ;;
|
|
122
|
+
*) untagged=$(( untagged + 1 )) ;;
|
|
123
|
+
esac
|
|
124
|
+
done
|
|
125
|
+
printf '%s %s %s %s %s' "$clauses" "$det" "$judgment" "$unenforced" "$untagged"
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
# The DETERMINISTIC codes a document's tags name, deduplicated, in the order
|
|
129
|
+
# they appear. This is the actionable half of the scoreboard: not "how many
|
|
130
|
+
# clauses exist" but "which rules here are decided by a script".
|
|
131
|
+
ledger_doc_codes() {
|
|
132
|
+
grep -oE "$DETERMINISTIC_TAG" "$1" 2>/dev/null \
|
|
133
|
+
| sed -E 's/.* → ([A-Za-z0-9_:-]+)\]/\1/' | sort -u
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
# The leaf audit a dispatch façade runs for a code, e.g. LOG → logging.sh. The
|
|
137
|
+
# .sh row is the single source of truth for which script decides a code, so a
|
|
138
|
+
# code that loses its helper stops claiming enforcement here the same commit.
|
|
139
|
+
ledger_code_script() {
|
|
140
|
+
local code="$1" script
|
|
141
|
+
script="$(grep -hoE "^dispatch_run_helper +\"$code\" +\"[^\"]+\"" "$LEDGER_ROOT"/dispatch/*.sh 2>/dev/null \
|
|
142
|
+
| sed -E 's/.*"([^"]+)"$/\1/' | head -1)"
|
|
143
|
+
printf '%s' "${script:-$UNWIRED_CODE_LABEL}"
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
# A DETERMINISTIC tag whose code has no helper row is a claim with nothing
|
|
147
|
+
# behind it — named rather than hidden, because that is exactly the drift this
|
|
148
|
+
# ledger exists to surface.
|
|
149
|
+
UNWIRED_CODE_LABEL="no helper row"
|
|
150
|
+
|
|
151
|
+
# docs/*.md paths any dispatch façade tells the agent to read. Both the bare
|
|
152
|
+
# form and the ~/Projects/dotfiles/-anchored form appear in the corpus, so the
|
|
153
|
+
# anchor is stripped before comparison.
|
|
154
|
+
ledger_cited_docs() {
|
|
155
|
+
grep -rhoE '(~/Projects/dotfiles/)?docs/[A-Za-z0-9_/-]+\.md' "$LEDGER_ROOT"/dispatch/*.md 2>/dev/null \
|
|
156
|
+
| sed -E 's|^~/Projects/dotfiles/||' \
|
|
157
|
+
| sort -u
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
# Membership test against a registry array passed by name-expansion.
|
|
161
|
+
ledger_in_list() {
|
|
162
|
+
local needle="$1"; shift
|
|
163
|
+
local item
|
|
164
|
+
for item in "$@"; do [ "$item" = "$needle" ] && return 0; done
|
|
165
|
+
return 1
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
# True when a cited path sits under a tree another repository owns.
|
|
169
|
+
ledger_has_prefix() {
|
|
170
|
+
local doc="$1" prefix
|
|
171
|
+
for prefix in "${EXCLUDED_PREFIXES[@]}"; do
|
|
172
|
+
case "$doc" in "$prefix"*) return 0 ;; esac
|
|
173
|
+
done
|
|
174
|
+
return 1
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
# --- trigger scope ------------------------------------------------------------
|
|
178
|
+
#
|
|
179
|
+
# A rule can only fire when something the agent edits matches its façade's
|
|
180
|
+
# scope, and that scope is declared exactly once: the `dispatch_init` line of
|
|
181
|
+
# the façade's executable. Reading it here means the ledger, the doc-read
|
|
182
|
+
# check, and the dispatcher itself can never disagree about what fires.
|
|
183
|
+
|
|
184
|
+
# dispatch/lib.sh is the shared sourcing target rather than a façade: it
|
|
185
|
+
# declares no scope of its own and must never be read as a glob-less one.
|
|
186
|
+
DISPATCH_LIB_BASENAME="lib.sh"
|
|
187
|
+
|
|
188
|
+
# The executable façades: dispatch/*.sh minus the shared library.
|
|
189
|
+
ledger_facade_scripts() {
|
|
190
|
+
local file
|
|
191
|
+
for file in "$LEDGER_ROOT"/dispatch/*.sh; do
|
|
192
|
+
[ -f "$file" ] || continue
|
|
193
|
+
[ "$(basename "$file")" = "$DISPATCH_LIB_BASENAME" ] && continue
|
|
194
|
+
printf '%s\n' "$file"
|
|
195
|
+
done
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
# The file globs a façade declares, space-separated. dispatch_init quotes its
|
|
199
|
+
# language double and every glob single, so the single-quoted run IS the scope.
|
|
200
|
+
# Empty output means the façade declares no scope — the caller reds on that.
|
|
201
|
+
ledger_facade_globs() {
|
|
202
|
+
grep -hE '^[[:space:]]*dispatch_init[[:space:]]' "$1" 2>/dev/null \
|
|
203
|
+
| head -1 | grep -oE "'[^']+'" | tr -d "'" | tr '\n' ' '
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
# How many paths on stdin fall inside a glob set. The unquoted `case` pattern
|
|
207
|
+
# is the match itself — the same expansion dispatch_resolve_files applies to
|
|
208
|
+
# explicit file arguments, so a fire count means what the dispatcher means.
|
|
209
|
+
ledger_match_count() {
|
|
210
|
+
local globs="$1" path glob matches=0
|
|
211
|
+
while IFS= read -r path; do
|
|
212
|
+
[ -n "$path" ] || continue
|
|
213
|
+
for glob in $globs; do
|
|
214
|
+
# shellcheck disable=SC2254 # the glob is data; expansion performs the match
|
|
215
|
+
case "$path" in $glob) matches=$((matches + 1)); break ;; esac
|
|
216
|
+
done
|
|
217
|
+
done
|
|
218
|
+
printf '%d' "$matches"
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
# The façade stems whose page cites a rule doc. Substring matching catches both
|
|
222
|
+
# the bare and the ~/Projects/dotfiles/-anchored spelling of the same path.
|
|
223
|
+
ledger_facades_citing() {
|
|
224
|
+
local doc="$1" page
|
|
225
|
+
for page in "$LEDGER_ROOT"/dispatch/*.md; do
|
|
226
|
+
[ -f "$page" ] || continue
|
|
227
|
+
grep -qF "$doc" "$page" 2>/dev/null && printf '%s\n' "$(basename "$page" .md)"
|
|
228
|
+
done
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
# Whether anything can carry a rule doc into a diff mechanically:
|
|
232
|
+
# mechanical — a citing façade declares a file scope
|
|
233
|
+
# latent — every citing façade is prose the agent must choose to read
|
|
234
|
+
# uncited — no façade page names the doc at all
|
|
235
|
+
# Tree-only, with no history in it, so the scoreboard stays a pure function of
|
|
236
|
+
# the checkout.
|
|
237
|
+
TRIGGER_MECHANICAL="mechanical"
|
|
238
|
+
TRIGGER_LATENT="latent"
|
|
239
|
+
TRIGGER_UNCITED="uncited"
|
|
240
|
+
|
|
241
|
+
ledger_doc_trigger() {
|
|
242
|
+
local doc="$1" stem script cited=1
|
|
243
|
+
while IFS= read -r stem; do
|
|
244
|
+
[ -n "$stem" ] || continue
|
|
245
|
+
cited=0
|
|
246
|
+
script="$LEDGER_ROOT/dispatch/$stem.sh"
|
|
247
|
+
[ -f "$script" ] || continue
|
|
248
|
+
[ -n "$(ledger_facade_globs "$script")" ] || continue
|
|
249
|
+
printf '%s' "$TRIGGER_MECHANICAL"
|
|
250
|
+
return
|
|
251
|
+
done < <(ledger_facades_citing "$doc")
|
|
252
|
+
[ "$cited" -eq 0 ] && { printf '%s' "$TRIGGER_LATENT"; return; }
|
|
253
|
+
printf '%s' "$TRIGGER_UNCITED"
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
# The citing façade stems as one comma-joined field, or an em dash when none.
|
|
257
|
+
ledger_doc_facade_list() {
|
|
258
|
+
local doc="$1" stem stems=""
|
|
259
|
+
while IFS= read -r stem; do
|
|
260
|
+
[ -n "$stem" ] || continue
|
|
261
|
+
stems="${stems:+$stems,}$stem"
|
|
262
|
+
done < <(ledger_facades_citing "$doc")
|
|
263
|
+
printf '%s' "${stems:-—}"
|
|
264
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# rule-ledger.sh — what each rule document actually enforces.
|
|
3
|
+
#
|
|
4
|
+
# audits/rule-ledger.sh --census one row per registered rule doc
|
|
5
|
+
#
|
|
6
|
+
# Reports the enforcement shape of the docs/ rule tier: how many normative
|
|
7
|
+
# clauses a document carries, and how many of those are machine-checked
|
|
8
|
+
# (DETERMINISTIC), agent-judged (JUDGMENT), acknowledged prose (UNENFORCED), or
|
|
9
|
+
# untriaged (untagged). Counts INFORM and never fail the build. The only census
|
|
10
|
+
# failure is STRUCTURAL: a rule doc cited by a dispatch façade that appears in
|
|
11
|
+
# neither the registry nor the exclusion list, or a registered path missing from
|
|
12
|
+
# disk — both mean the ledger is lying about coverage.
|
|
13
|
+
#
|
|
14
|
+
# The census answers the other half too: a rule that reads perfectly still
|
|
15
|
+
# governs nothing if its façade declares no scope, because nothing it says can
|
|
16
|
+
# ever reach a diff. That is structural, and answerable from the tree.
|
|
17
|
+
#
|
|
18
|
+
# Exit: 0 clean · 1 structural violation · 2 usage error. Writes nothing.
|
|
19
|
+
set -uo pipefail
|
|
20
|
+
|
|
21
|
+
# `sort` orders by locale, and this script's output is byte-compared by
|
|
22
|
+
# `make audit`. Without a pinned collation the scoreboard is a function of the
|
|
23
|
+
# tree AND the contributor's LC_ALL: `LC_ALL=C` sorts `A-Z B-X a_y`, en_US.UTF-8
|
|
24
|
+
# sorts `a_y A-Z B-X`. Invisible while every document carries one code, and a
|
|
25
|
+
# guaranteed spurious red the moment one carries two.
|
|
26
|
+
export LC_ALL=C
|
|
27
|
+
|
|
28
|
+
# A hook exports GIT_DIR/GIT_INDEX_FILE at the repository it fired in; any git
|
|
29
|
+
# a child runs would silently retarget there. The library resolves LEDGER_ROOT
|
|
30
|
+
# with git, so the scope dies here, before it is sourced.
|
|
31
|
+
unset GIT_DIR GIT_INDEX_FILE GIT_WORK_TREE GIT_COMMON_DIR GIT_PREFIX \
|
|
32
|
+
GIT_OBJECT_DIRECTORY GIT_ALTERNATE_OBJECT_DIRECTORIES
|
|
33
|
+
|
|
34
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
35
|
+
# shellcheck source=audits/rule-ledger-lib.sh
|
|
36
|
+
source "$HERE/rule-ledger-lib.sh"
|
|
37
|
+
|
|
38
|
+
MODE_CENSUS="census"
|
|
39
|
+
MODE_WRITE="write"
|
|
40
|
+
MODE_CHECK="check"
|
|
41
|
+
SCOREBOARD_PATH="docs/RULE_ENFORCEMENT.md"
|
|
42
|
+
SCOREBOARD_FIX_COMMAND="bash audits/rule-ledger.sh --write $SCOREBOARD_PATH"
|
|
43
|
+
USAGE="usage: $0 --census | --write <file> | --check"
|
|
44
|
+
|
|
45
|
+
MODE=""
|
|
46
|
+
WRITE_TARGET=""
|
|
47
|
+
while [ "$#" -gt 0 ]; do
|
|
48
|
+
case "$1" in
|
|
49
|
+
--census) MODE="$MODE_CENSUS" ;;
|
|
50
|
+
--check) MODE="$MODE_CHECK" ;;
|
|
51
|
+
--write)
|
|
52
|
+
shift
|
|
53
|
+
WRITE_TARGET="${1:-}"
|
|
54
|
+
[ -n "$WRITE_TARGET" ] || { printf '%s\n--write takes a target file\n' "$USAGE" >&2; exit 2; }
|
|
55
|
+
MODE="$MODE_WRITE"
|
|
56
|
+
;;
|
|
57
|
+
-h|--help) printf '%s\n' "$USAGE"; exit 0 ;;
|
|
58
|
+
*) printf 'unknown arg: %s\n%s\n' "$1" "$USAGE" >&2; exit 2 ;;
|
|
59
|
+
esac
|
|
60
|
+
shift
|
|
61
|
+
done
|
|
62
|
+
[ -n "$MODE" ] || { printf '%s\n' "$USAGE" >&2; exit 2; }
|
|
63
|
+
|
|
64
|
+
if [[ -t 1 ]]; then G=$'\033[32m'; R=$'\033[31m'; BO=$'\033[1m'; X=$'\033[0m'
|
|
65
|
+
else G=''; R=''; BO=''; X=''; fi
|
|
66
|
+
RC=0
|
|
67
|
+
fail() { printf ' %s🔴 %s%s\n' "$R" "$1" "$X" >&2; RC=1; }
|
|
68
|
+
okln() { printf ' %s🟢%s %s\n' "$G" "$X" "$1"; }
|
|
69
|
+
|
|
70
|
+
# One machine-parseable row per registered document. Field order is stable and
|
|
71
|
+
# every value is a bare integer, so a caller greps `untagged=0` rather than
|
|
72
|
+
# parsing a table.
|
|
73
|
+
census_row() {
|
|
74
|
+
local doc="$1"
|
|
75
|
+
local counts clauses det judgment unenforced untagged
|
|
76
|
+
if ! counts="$(ledger_doc_counts "$LEDGER_ROOT/$doc")"; then
|
|
77
|
+
fail "doc=$doc MISSING from disk — registered in rule-ledger-lib.sh REGISTERED_DOCS"
|
|
78
|
+
return
|
|
79
|
+
fi
|
|
80
|
+
read -r clauses det judgment unenforced untagged <<< "$counts"
|
|
81
|
+
printf ' doc=%-44s clauses=%-4s det=%-4s judgment=%-4s unenforced=%-4s untagged=%s\n' \
|
|
82
|
+
"$doc" "$clauses" "$det" "$judgment" "$unenforced" "$untagged"
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
# A façade that tells the agent to read a rule doc the ledger does not know
|
|
86
|
+
# about means the scoreboard under-reports the corpus. Either register it or
|
|
87
|
+
# declare it a non-rule document — silence is the one outcome not allowed.
|
|
88
|
+
check_registry_parity() {
|
|
89
|
+
local doc
|
|
90
|
+
while IFS= read -r doc; do
|
|
91
|
+
[ -n "$doc" ] || continue
|
|
92
|
+
ledger_in_list "$doc" "${REGISTERED_DOCS[@]}" && continue
|
|
93
|
+
ledger_in_list "$doc" "${EXCLUDED_DOCS[@]}" && continue
|
|
94
|
+
ledger_has_prefix "$doc" && continue
|
|
95
|
+
fail "doc=$doc cited by a dispatch façade but in neither REGISTERED_DOCS nor EXCLUDED_DOCS (audits/rule-ledger-lib.sh)"
|
|
96
|
+
done < <(ledger_cited_docs)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
# A façade executable that declares no `dispatch_init` scope carries rules no
|
|
100
|
+
# diff can ever reach. It is the one trigger question worth failing on, and it
|
|
101
|
+
# is answerable from the tree — unlike "has this fired lately", which depends on
|
|
102
|
+
# what happened to get committed recently and gates nothing.
|
|
103
|
+
check_facade_scopes() {
|
|
104
|
+
local script stem
|
|
105
|
+
while IFS= read -r script; do
|
|
106
|
+
[ -n "$script" ] || continue
|
|
107
|
+
[ -n "$(ledger_facade_globs "$script")" ] && continue
|
|
108
|
+
stem="$(basename "$script" .sh)"
|
|
109
|
+
fail "facade=$stem declares no dispatch_init scope — dispatch/$stem.sh (nothing it carries can ever fire)"
|
|
110
|
+
done < <(ledger_facade_scripts)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
run_census() {
|
|
114
|
+
printf '%sRULE ENFORCEMENT LEDGER — census%s\n' "$BO" "$X"
|
|
115
|
+
for entry in "${REGISTERED_DOCS[@]}"; do census_row "$entry"; done
|
|
116
|
+
printf '\n%sstructural checks%s\n' "$BO" "$X"
|
|
117
|
+
check_registry_parity
|
|
118
|
+
check_facade_scopes
|
|
119
|
+
[ "$RC" -eq 0 ] && okln "every cited rule doc is registered; every façade declares a scope"
|
|
120
|
+
return 0
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
scoreboard_legend() {
|
|
124
|
+
cat <<'LEGEND'
|
|
125
|
+
## How to read this
|
|
126
|
+
|
|
127
|
+
- **enforced by** — the rules in this document a script decides, and the script
|
|
128
|
+
that decides each. This is the column worth acting on: it is the difference
|
|
129
|
+
between a rule that holds and a rule that is merely written down. A code
|
|
130
|
+
reading `no helper row` is a `[DETERMINISTIC → …]` tag with nothing behind it.
|
|
131
|
+
- **judged / acknowledged** — clauses an agent must weigh (`[JUDGMENT → CODE]`)
|
|
132
|
+
and clauses nobody checks on purpose (`[UNENFORCED → reason]`, the reason
|
|
133
|
+
stated where a reader meets it).
|
|
134
|
+
- **classified** — how much of the document has been triaged at all. The
|
|
135
|
+
denominator counts lines carrying a normative keyword outside headings and
|
|
136
|
+
tables — a keyword heuristic, not a parse. It informs; it never gates.
|
|
137
|
+
- **trigger** — `mechanical` when a citing façade declares a file scope,
|
|
138
|
+
`latent` when every citing façade is prose the agent must choose to read,
|
|
139
|
+
`uncited` when no façade names the document at all.
|
|
140
|
+
|
|
141
|
+
A tag alone on its line covers every clause under it until the next heading; a
|
|
142
|
+
tag at the end of a sentence covers that sentence. Same grammar the
|
|
143
|
+
`dispatch/*.md` façades use, read by the same code.
|
|
144
|
+
|
|
145
|
+
Everything here is a function of the tree alone — no timestamps, no commit
|
|
146
|
+
hashes, no history — so the file reproduces from a checkout and any diff means
|
|
147
|
+
the corpus moved.
|
|
148
|
+
LEGEND
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
# The scoreboard body: what each document actually enforces, then how much of it
|
|
152
|
+
# has been triaged. Deterministic by construction — registry order in, content
|
|
153
|
+
# out — so `--check` can regenerate and byte-compare.
|
|
154
|
+
render_scoreboard() {
|
|
155
|
+
local doc counts clauses det judgment unenforced untagged code codes
|
|
156
|
+
local sum_clauses=0 sum_classified=0
|
|
157
|
+
printf '# Rule enforcement ledger\n\n'
|
|
158
|
+
printf 'Generated by `audits/rule-ledger.sh`. Never hand-edit: `make audit`\n'
|
|
159
|
+
printf 'regenerates this file and fails on any difference. To update it, run\n'
|
|
160
|
+
printf '`%s`.\n\n' "$SCOREBOARD_FIX_COMMAND"
|
|
161
|
+
printf '| Rule document | enforced by | judged | acknowledged | classified | trigger |\n'
|
|
162
|
+
printf '|---|---|--:|--:|--:|---|\n'
|
|
163
|
+
for doc in "${REGISTERED_DOCS[@]}"; do
|
|
164
|
+
counts="$(ledger_doc_counts "$LEDGER_ROOT/$doc")" || return 1
|
|
165
|
+
read -r clauses det judgment unenforced untagged <<< "$counts"
|
|
166
|
+
codes=""
|
|
167
|
+
while IFS= read -r code; do
|
|
168
|
+
[ -n "$code" ] || continue
|
|
169
|
+
codes="${codes:+$codes<br>}\`$code\` → \`$(ledger_code_script "$code")\`"
|
|
170
|
+
done < <(ledger_doc_codes "$LEDGER_ROOT/$doc")
|
|
171
|
+
printf '| `%s` | %s | %s | %s | %s/%s | %s |\n' \
|
|
172
|
+
"$doc" "${codes:-—}" "$judgment" "$unenforced" \
|
|
173
|
+
"$(( clauses - untagged ))" "$clauses" "$(ledger_doc_trigger "$doc")"
|
|
174
|
+
sum_clauses=$(( sum_clauses + clauses ))
|
|
175
|
+
sum_classified=$(( sum_classified + clauses - untagged ))
|
|
176
|
+
done
|
|
177
|
+
printf '| **corpus** | | | | **%s/%s** | |\n\n' "$sum_classified" "$sum_clauses"
|
|
178
|
+
scoreboard_legend
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
run_write() {
|
|
182
|
+
local target="$1" rendered
|
|
183
|
+
case "$target" in /*) ;; *) target="$LEDGER_ROOT/$target" ;; esac
|
|
184
|
+
rendered="$(mktemp)"
|
|
185
|
+
trap 'rm -f "$rendered"' EXIT
|
|
186
|
+
if render_scoreboard > "$rendered"; then
|
|
187
|
+
cat "$rendered" > "$target"
|
|
188
|
+
printf 'wrote %s\n' "$target"
|
|
189
|
+
else
|
|
190
|
+
fail "render aborted — a registered doc is missing from disk; run --census for the name"
|
|
191
|
+
fi
|
|
192
|
+
rm -f "$rendered"
|
|
193
|
+
trap - EXIT
|
|
194
|
+
return 0
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
# Currency, not content: the committed file must equal what this tree renders.
|
|
198
|
+
# A stale scoreboard is a red because a wrong number read as fact is worse than
|
|
199
|
+
# no number at all.
|
|
200
|
+
run_check() {
|
|
201
|
+
local target="$LEDGER_ROOT/$SCOREBOARD_PATH" rendered
|
|
202
|
+
rendered="$(mktemp)"
|
|
203
|
+
trap 'rm -f "$rendered"' EXIT
|
|
204
|
+
printf '%sRULE ENFORCEMENT LEDGER — scoreboard currency%s\n' "$BO" "$X"
|
|
205
|
+
if ! render_scoreboard > "$rendered"; then
|
|
206
|
+
fail "a registered doc is missing from disk — run --census for the name"
|
|
207
|
+
elif [ ! -f "$target" ]; then
|
|
208
|
+
fail "$SCOREBOARD_PATH does not exist — regenerate: $SCOREBOARD_FIX_COMMAND"
|
|
209
|
+
elif ! cmp -s "$rendered" "$target"; then
|
|
210
|
+
fail "$SCOREBOARD_PATH is stale — regenerate: $SCOREBOARD_FIX_COMMAND"
|
|
211
|
+
else
|
|
212
|
+
okln "$SCOREBOARD_PATH matches the tree"
|
|
213
|
+
fi
|
|
214
|
+
rm -f "$rendered"
|
|
215
|
+
trap - EXIT
|
|
216
|
+
return 0
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
case "$MODE" in
|
|
220
|
+
"$MODE_CENSUS") run_census ;;
|
|
221
|
+
"$MODE_WRITE") run_write "$WRITE_TARGET" ;;
|
|
222
|
+
"$MODE_CHECK") run_check ;;
|
|
223
|
+
esac
|
|
224
|
+
|
|
225
|
+
exit "$RC"
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# rule-paths.sh — residence checks: rule citations resolve inside the repository
|
|
3
|
+
# that installed them.
|
|
4
|
+
#
|
|
5
|
+
# The incident this encodes (M143): an agent in an agentsfleet worktree
|
|
6
|
+
# resolved "reads docs/REST_API_DESIGN_GUIDELINES.md" against its own
|
|
7
|
+
# repository, found nothing — under the old thin-distribution model, consumer
|
|
8
|
+
# repos carried no rule copies at all — and designed an HTTP surface from
|
|
9
|
+
# memory instead of the guide.
|
|
10
|
+
#
|
|
11
|
+
# M03_001 closed that gap by materialising rule docs into every consuming
|
|
12
|
+
# repository (`orly init`/`orly update`, keyed by the profile's selected
|
|
13
|
+
# packs). A citation now resolving to a real local file IS the fix, so the
|
|
14
|
+
# invariant inverts: the ~/Projects/dotfiles/ anchor becomes wrong wherever
|
|
15
|
+
# it appears in a surface a materialised repository also carries — it names a
|
|
16
|
+
# path that will not exist on any machine but this one.
|
|
17
|
+
#
|
|
18
|
+
# residence — dotfiles-resident rule docs are cited relative to the
|
|
19
|
+
# installing repository, never through the
|
|
20
|
+
# ~/Projects/dotfiles/ anchor, in every surface `orly init`
|
|
21
|
+
# materialises. AGENTS.md (byte-capped) carries the new
|
|
22
|
+
# resolution doctrine, zero CWD-relative hrefs, and no
|
|
23
|
+
# residence-contradicting rows. Files that never leave this
|
|
24
|
+
# checkout (RULE_PATH_ENGINE_ONLY — governance-editing
|
|
25
|
+
# façades genuinely about the engine's own source) are
|
|
26
|
+
# exempt; their absolute citations are correct.
|
|
27
|
+
# (Reachability was the other half of this file: a settings allow-rule
|
|
28
|
+
# granting Read(~/Projects/dotfiles/**) so an agent could read rule docs out
|
|
29
|
+
# of a dotfiles checkout. M03_001 inverted the model — rules resolve inside
|
|
30
|
+
# the repository that installed them — and the engine now lives in its own
|
|
31
|
+
# repository, where there is no dotfiles anchor to reach and no personal
|
|
32
|
+
# settings file to assert against. It went with the split.)
|
|
33
|
+
#
|
|
34
|
+
# Sourced by audits/agents-md.sh (labels: "rule-path residence",
|
|
35
|
+
# Classifier: DOTFILES_RESIDENT in audits/data.sh.
|
|
36
|
+
#
|
|
37
|
+
# check_rule_residence <ROOT> # echoes FAIL lines, returns 0/1
|
|
38
|
+
|
|
39
|
+
# Non-dispatch surfaces read from consumer-repo CWDs; dispatch/*.md is globbed.
|
|
40
|
+
RULE_PATH_SURFACES=(
|
|
41
|
+
"docs/TEMPLATE.md"
|
|
42
|
+
"docs/EXECUTE_DOC_READS.md"
|
|
43
|
+
"docs/greptile-learnings/RULES.md"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# Governance-editing façades describe how to edit THIS engine's own source
|
|
47
|
+
# (core/**, packs/**, schemas/**, src/**, the registry, the rendered AGENTS.md) — a workflow that only ever
|
|
48
|
+
# runs inside this checkout. Their absolute citations are correct, not a
|
|
49
|
+
# residual of the old model; a materialised copy would never satisfy them
|
|
50
|
+
# because there is nothing to materialise (a consumer never receives
|
|
51
|
+
# core/, registry.json, or this checkout's own dispatch/edit_rules.md).
|
|
52
|
+
RULE_PATH_ENGINE_ONLY=(
|
|
53
|
+
"dispatch/edit_rules.md"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
check_rule_residence() {
|
|
57
|
+
local root="$1" rc=0
|
|
58
|
+
local agents="$root/AGENTS.md" # separate line: $root must be set before it expands here
|
|
59
|
+
|
|
60
|
+
# (a) resolution doctrine present in the rendered rules.
|
|
61
|
+
grep -qF 'Rule paths resolve relative to this repository' "$agents" \
|
|
62
|
+
|| { echo "FAIL: resolution doctrine missing from AGENTS.md"; rc=1; }
|
|
63
|
+
|
|
64
|
+
# (b) zero CWD-relative markdown hrefs — AGENTS.md is served as
|
|
65
|
+
# ~/.claude/CLAUDE.md, so ](./…) and ](../…) resolve against garbage.
|
|
66
|
+
if grep -qE '\]\((\./|\.\./)' "$agents"; then
|
|
67
|
+
echo "FAIL: AGENTS.md carries $(grep -cE '\]\((\./|\.\./)' "$agents") CWD-relative markdown href(s)"
|
|
68
|
+
rc=1
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
# Resident docs/… names derived from DOTFILES_RESIDENT (single classifier).
|
|
72
|
+
local d names=()
|
|
73
|
+
for d in "${DOTFILES_RESIDENT[@]}"; do
|
|
74
|
+
[[ "$d" == docs/* ]] && names+=("${d#docs/}")
|
|
75
|
+
done
|
|
76
|
+
|
|
77
|
+
# (c) no AGENTS.md line may pair a resident doc with "(product repo)" — the
|
|
78
|
+
# misread that sent the M143 agent to the wrong repository. Still a real
|
|
79
|
+
# mistake under the new model: a doc this array names IS materialised here.
|
|
80
|
+
local alt
|
|
81
|
+
alt=$(IFS='|'; printf '%s' "${names[*]%.md}")
|
|
82
|
+
if grep -E "docs/(${alt})\.md" "$agents" | grep -qF '(product repo)'; then
|
|
83
|
+
echo "FAIL: AGENTS.md line pairs a dotfiles-resident doc with '(product repo)'"
|
|
84
|
+
rc=1
|
|
85
|
+
fi
|
|
86
|
+
|
|
87
|
+
# (d) surfaces materialised by orly init must cite resident docs relative to
|
|
88
|
+
# the installing repository — never through the ~/Projects/dotfiles/ anchor,
|
|
89
|
+
# which names a path absent on every machine but this one. Engine-only
|
|
90
|
+
# façades (RULE_PATH_ENGINE_ONLY) are exempt: their absolute citations are
|
|
91
|
+
# correct because nothing is ever materialised to satisfy them.
|
|
92
|
+
local f rel is_exempt e hits
|
|
93
|
+
for f in "$agents" "$root"/dispatch/*.md "${RULE_PATH_SURFACES[@]/#/${root}/}"; do
|
|
94
|
+
[[ -f "$f" ]] || continue
|
|
95
|
+
rel="${f#"$root"/}"
|
|
96
|
+
is_exempt=0
|
|
97
|
+
for e in "${RULE_PATH_ENGINE_ONLY[@]}"; do [[ "$e" == "$rel" ]] && is_exempt=1; done
|
|
98
|
+
[[ "$is_exempt" -eq 1 ]] && continue
|
|
99
|
+
hits=$(LC_ALL=C grep -noE '~/Projects/dotfiles/(docs|dispatch|audits)/[A-Za-z0-9_./-]+\.(md|sh)' "$f" | head -5)
|
|
100
|
+
if [[ -n "$hits" ]]; then
|
|
101
|
+
echo "FAIL: anchored citation in $rel (should resolve relative to the installing repository):"
|
|
102
|
+
printf ' %s\n' "$hits"
|
|
103
|
+
rc=1
|
|
104
|
+
fi
|
|
105
|
+
done
|
|
106
|
+
|
|
107
|
+
return $rc
|
|
108
|
+
}
|