@thebassclef/lite 0.1.3 → 1.0.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 (293) hide show
  1. package/dist/cli.cjs +216 -137
  2. package/dist/cli.js +218 -139
  3. package/dist/index.cjs +1 -1
  4. package/dist/index.d.ts +1 -1
  5. package/dist/index.js +1 -1
  6. package/dist/lite/.bassclef-source.json +10 -0
  7. package/dist/lite/.claude/settings.json +212 -0
  8. package/dist/lite/CLAUDE.md +41 -0
  9. package/dist/lite/gitignore +58 -0
  10. package/dist/lite/standards/bassclef-wiring-manifest.json +497 -0
  11. package/dist/lite/whereami.md +24 -0
  12. package/package.json +8 -2
  13. package/substrate/.bassclef/lite-manifest.json +0 -2701
  14. package/substrate/.claude/agents/architect.md +0 -70
  15. package/substrate/.claude/agents/builder.md +0 -114
  16. package/substrate/.claude/agents/designer.md +0 -156
  17. package/substrate/.claude/agents/reviewer.md +0 -88
  18. package/substrate/.claude/hooks/artifact-ingestion-gate.sh +0 -357
  19. package/substrate/.claude/hooks/assert-verify-steering.sh +0 -77
  20. package/substrate/.claude/hooks/bassclef-source-config-validate.sh +0 -215
  21. package/substrate/.claude/hooks/bassclef-sync.sh +0 -634
  22. package/substrate/.claude/hooks/compound-noun-scrub.sh +0 -292
  23. package/substrate/.claude/hooks/kiss-expansion-inject.sh +0 -69
  24. package/substrate/.claude/hooks/longrun-prep-compounding-axis-check.sh +0 -492
  25. package/substrate/.claude/hooks/longrun-prep-compounding-sequence-check.sh +0 -492
  26. package/substrate/.claude/hooks/plain-english-steering.sh +0 -156
  27. package/substrate/.claude/hooks/post-skill-friction-check.sh +0 -177
  28. package/substrate/.claude/hooks/post-skill-telemetry.sh +0 -62
  29. package/substrate/.claude/hooks/pre-build-gate.sh +0 -511
  30. package/substrate/.claude/hooks/pre-commit-gate.sh +0 -451
  31. package/substrate/.claude/hooks/session-end.sh +0 -433
  32. package/substrate/.claude/hooks/session-reflection.sh +0 -303
  33. package/substrate/.claude/hooks/skill-body-grade-gate.sh +0 -219
  34. package/substrate/.claude/hooks/skill-body-intent-drift.sh +0 -107
  35. package/substrate/.claude/hooks/skill-step-list-check.sh +0 -171
  36. package/substrate/.claude/hooks/state-validate.sh +0 -271
  37. package/substrate/.claude/hooks/substrate-clarity-gate.sh +0 -1110
  38. package/substrate/.claude/hooks/temperance-gate.sh +0 -147
  39. package/substrate/.claude/hooks/testing-tier-enforce.sh +0 -233
  40. package/substrate/.claude/hooks/turn-prose-grade-measure.sh +0 -219
  41. package/substrate/.claude/hooks/turn-prose-kiss-check.sh +0 -463
  42. package/substrate/.claude/hooks/vocabulary-migration-check.sh +0 -171
  43. package/substrate/.claude/hooks/whereami-utc-gate.sh +0 -142
  44. package/substrate/.claude/luminaries/alan-cooper.md +0 -170
  45. package/substrate/.claude/luminaries/alistair-cockburn.md +0 -140
  46. package/substrate/.claude/luminaries/amazon-pr-faq.md +0 -34
  47. package/substrate/.claude/luminaries/ash-maurya.md +0 -121
  48. package/substrate/.claude/luminaries/bill-buxton.md +0 -210
  49. package/substrate/.claude/luminaries/charles-sanders-peirce.md +0 -150
  50. package/substrate/.claude/luminaries/david-ogilvy.md +0 -192
  51. package/substrate/.claude/luminaries/don-norman.md +0 -173
  52. package/substrate/.claude/luminaries/edward-tufte.md +0 -179
  53. package/substrate/.claude/luminaries/eric-evans.md +0 -160
  54. package/substrate/.claude/luminaries/frederick-brooks.md +0 -50
  55. package/substrate/.claude/luminaries/gang-of-four.md +0 -157
  56. package/substrate/.claude/luminaries/glenford-myers.md +0 -230
  57. package/substrate/.claude/luminaries/hunt-thomas.md +0 -115
  58. package/substrate/.claude/luminaries/hyrum-wright.md +0 -94
  59. package/substrate/.claude/luminaries/jason-fried-dhh.md +0 -46
  60. package/substrate/.claude/luminaries/jesse-james-garrett.md +0 -154
  61. package/substrate/.claude/luminaries/john-ousterhout.md +0 -94
  62. package/substrate/.claude/luminaries/karl-popper.md +0 -132
  63. package/substrate/.claude/luminaries/kent-beck.md +0 -168
  64. package/substrate/.claude/luminaries/linus-torvalds.md +0 -218
  65. package/substrate/.claude/luminaries/martin-fowler.md +0 -164
  66. package/substrate/.claude/luminaries/michael-feathers.md +0 -125
  67. package/substrate/.claude/luminaries/michael-nygard.md +0 -118
  68. package/substrate/.claude/luminaries/robert-c-martin.md +0 -164
  69. package/substrate/.claude/luminaries/saltzer-schroeder.md +0 -194
  70. package/substrate/.claude/luminaries/sophia-prater.md +0 -193
  71. package/substrate/.claude/luminaries/stephen-toulmin.md +0 -131
  72. package/substrate/.claude/luminaries/tony-hoare.md +0 -170
  73. package/substrate/.claude/luminaries/vaughn-vernon.md +0 -50
  74. package/substrate/.claude/luminaries/w-edwards-deming.md +0 -158
  75. package/substrate/.claude/rules/accessor-library-discipline.md +0 -138
  76. package/substrate/.claude/rules/adr-discipline.md +0 -120
  77. package/substrate/.claude/rules/api-conventions.md +0 -125
  78. package/substrate/.claude/rules/artifact-ingestion.md +0 -179
  79. package/substrate/.claude/rules/assert-only-after-verify.md +0 -137
  80. package/substrate/.claude/rules/blocked-items.md +0 -146
  81. package/substrate/.claude/rules/bootstrap-pair-discipline.md +0 -141
  82. package/substrate/.claude/rules/branching.md +0 -28
  83. package/substrate/.claude/rules/cold-adopter-harness-discipline.md +0 -129
  84. package/substrate/.claude/rules/commit-conventions.md +0 -22
  85. package/substrate/.claude/rules/compounding-axis-fresh-analysis.md +0 -188
  86. package/substrate/.claude/rules/compounding-sequence-fresh-analysis.md +0 -188
  87. package/substrate/.claude/rules/context-engineering.md +0 -202
  88. package/substrate/.claude/rules/context-management.md +0 -85
  89. package/substrate/.claude/rules/defensive-bash.md +0 -68
  90. package/substrate/.claude/rules/deferred-actions.md +0 -233
  91. package/substrate/.claude/rules/destructive-operations.md +0 -69
  92. package/substrate/.claude/rules/diagnosis.md +0 -38
  93. package/substrate/.claude/rules/github-issue-flash-tweet.md +0 -156
  94. package/substrate/.claude/rules/guardrails.md +0 -73
  95. package/substrate/.claude/rules/hook-wire-on-author.md +0 -103
  96. package/substrate/.claude/rules/identifier-leak-prevention.md +0 -104
  97. package/substrate/.claude/rules/iteration-bet-brief-completeness.md +0 -54
  98. package/substrate/.claude/rules/lite-manifest-schema-change-discipline.md +0 -98
  99. package/substrate/.claude/rules/longrun-prep-plan-doc-compression.md +0 -89
  100. package/substrate/.claude/rules/loop-discipline.md +0 -81
  101. package/substrate/.claude/rules/manual-prod-approval.md +0 -100
  102. package/substrate/.claude/rules/marker-enrichment-discipline.md +0 -99
  103. package/substrate/.claude/rules/mobile-ephemeral-session.md +0 -109
  104. package/substrate/.claude/rules/new-dependency-check.md +0 -51
  105. package/substrate/.claude/rules/oo-ad-entry-point.md +0 -117
  106. package/substrate/.claude/rules/operator-facing-prose.md +0 -196
  107. package/substrate/.claude/rules/option-label-discipline.md +0 -108
  108. package/substrate/.claude/rules/pattern-annotation.md +0 -100
  109. package/substrate/.claude/rules/plain-english-discipline.md +0 -156
  110. package/substrate/.claude/rules/plan-enumeration-needs-value-props.md +0 -211
  111. package/substrate/.claude/rules/pr-body-shape.md +0 -317
  112. package/substrate/.claude/rules/pr-strategy.md +0 -167
  113. package/substrate/.claude/rules/pr-title-shape.md +0 -161
  114. package/substrate/.claude/rules/prototype-workflow.md +0 -65
  115. package/substrate/.claude/rules/reserved-skill-names.md +0 -123
  116. package/substrate/.claude/rules/schema-management.md +0 -49
  117. package/substrate/.claude/rules/sdlc-gates.md +0 -149
  118. package/substrate/.claude/rules/security.md +0 -37
  119. package/substrate/.claude/rules/session-artifacts.md +0 -236
  120. package/substrate/.claude/rules/skill-composition-declarations.md +0 -124
  121. package/substrate/.claude/rules/skill-description-clarity.md +0 -247
  122. package/substrate/.claude/rules/skill-procedure-step-list.md +0 -137
  123. package/substrate/.claude/rules/state-schema-validation.md +0 -162
  124. package/substrate/.claude/rules/stuck-signal-diagnostic.md +0 -140
  125. package/substrate/.claude/rules/substrate-config-schema.md +0 -98
  126. package/substrate/.claude/rules/test-list-discipline.md +0 -175
  127. package/substrate/.claude/rules/test-sufficiency.md +0 -210
  128. package/substrate/.claude/rules/testing-tier-config.md +0 -145
  129. package/substrate/.claude/rules/testing.md +0 -38
  130. package/substrate/.claude/rules/turn-estimate-grounding.md +0 -134
  131. package/substrate/.claude/rules/visual-hierarchy.md +0 -437
  132. package/substrate/.claude/rules/we-dont-break-adopters.md +0 -126
  133. package/substrate/.claude/rules/whereami-load-bearing.md +0 -202
  134. package/substrate/.claude/rules/writing-craft-discipline.md +0 -92
  135. package/substrate/.claude/rules/wu-sequencing-compounds.md +0 -145
  136. package/substrate/.claude/skills/build/SKILL.md +0 -640
  137. package/substrate/.claude/skills/chronicle/SKILL.md +0 -55
  138. package/substrate/.claude/skills/clean-artifacts/SKILL.md +0 -249
  139. package/substrate/.claude/skills/decompose/SKILL.md +0 -280
  140. package/substrate/.claude/skills/diagnose/SKILL.md +0 -297
  141. package/substrate/.claude/skills/feynman/SKILL.md +0 -90
  142. package/substrate/.claude/skills/howdoi/SKILL.md +0 -105
  143. package/substrate/.claude/skills/ia-model/SKILL.md +0 -108
  144. package/substrate/.claude/skills/interaction-design/SKILL.md +0 -112
  145. package/substrate/.claude/skills/interpret-input/SKILL.md +0 -180
  146. package/substrate/.claude/skills/journal/SKILL.md +0 -209
  147. package/substrate/.claude/skills/kiss/SKILL.md +0 -449
  148. package/substrate/.claude/skills/launch/SKILL.md +0 -915
  149. package/substrate/.claude/skills/lean-canvas/SKILL.md +0 -332
  150. package/substrate/.claude/skills/longrun/SKILL.md +0 -463
  151. package/substrate/.claude/skills/luminary/SKILL.md +0 -481
  152. package/substrate/.claude/skills/ogilvy-writing-audit/SKILL.md +0 -177
  153. package/substrate/.claude/skills/onboard-repo/SKILL.md +0 -1624
  154. package/substrate/.claude/skills/pattern-review/SKILL.md +0 -99
  155. package/substrate/.claude/skills/personas/SKILL.md +0 -207
  156. package/substrate/.claude/skills/promote/SKILL.md +0 -283
  157. package/substrate/.claude/skills/requirement/SKILL.md +0 -98
  158. package/substrate/.claude/skills/retro/SKILL.md +0 -117
  159. package/substrate/.claude/skills/riff/SKILL.md +0 -114
  160. package/substrate/.claude/skills/roadmap-reconcile/SKILL.md +0 -163
  161. package/substrate/.claude/skills/session-end/SKILL.md +0 -309
  162. package/substrate/.claude/skills/session-log/SKILL.md +0 -299
  163. package/substrate/.claude/skills/skills/SKILL.md +0 -228
  164. package/substrate/.claude/skills/spec/SKILL.md +0 -105
  165. package/substrate/.claude/skills/sprint/SKILL.md +0 -392
  166. package/substrate/.claude/skills/stage/SKILL.md +0 -384
  167. package/substrate/.claude/skills/state-a-problem/SKILL.md +0 -185
  168. package/substrate/.claude/skills/temperance/SKILL.md +0 -108
  169. package/substrate/.claude/skills/use-case/SKILL.md +0 -417
  170. package/substrate/.claude/skills/user-stories/SKILL.md +0 -268
  171. package/substrate/.claude/skills/value-prop/SKILL.md +0 -251
  172. package/substrate/.claude/skills/verify/SKILL.md +0 -160
  173. package/substrate/.claude/skills/visual-review/SKILL.md +0 -503
  174. package/substrate/.claude/skills/whats-the-plan/SKILL.md +0 -202
  175. package/substrate/.claude/skills/whereami/SKILL.md +0 -307
  176. package/substrate/AGENTS.md +0 -79
  177. package/substrate/CLAUDE-lite.md +0 -85
  178. package/substrate/CODE_OF_CONDUCT.md +0 -28
  179. package/substrate/CONTRIBUTING.md +0 -177
  180. package/substrate/README.md +0 -173
  181. package/substrate/SECURITY.md +0 -19
  182. package/substrate/architecture/decisions/ADR-029-release-pipeline.md +0 -79
  183. package/substrate/architecture/decisions/ADR-031-non-breaking-changes-adopter-discipline.md +0 -139
  184. package/substrate/architecture/decisions/ADR-032-adopter-sync-dispatcher-architecture.md +0 -192
  185. package/substrate/architecture/decisions/ADR-039-release-tagging-scheme.md +0 -145
  186. package/substrate/architecture/decisions/ADR-040-planning-skill-vocabulary-and-lite-profile.md +0 -155
  187. package/substrate/architecture/decisions/ADR-044-unified-skill-body-template.md +0 -162
  188. package/substrate/lib/clean-artifacts-sweep.sh +0 -112
  189. package/substrate/lib/code-comment-discipline.sh +0 -144
  190. package/substrate/lib/composer-preflight.sh +0 -459
  191. package/substrate/lib/hook-inject.sh +0 -255
  192. package/substrate/lib/luminary-pick.sh +0 -96
  193. package/substrate/lib/output-discipline.sh +0 -143
  194. package/substrate/lib/prose-scan-boundary.sh +0 -171
  195. package/substrate/lib/rewrite-check.sh +0 -214
  196. package/substrate/lib/state.sh +0 -1372
  197. package/substrate/lib/telemetry.sh +0 -205
  198. package/substrate/lib/tier-check.sh +0 -187
  199. package/substrate/lib/tier-dependency-audit.sh +0 -1088
  200. package/substrate/presence/install/bassclef-hook-connect.sh +0 -178
  201. package/substrate/presence/install/bassclef-sync.dispatcher.template.sh +0 -841
  202. package/substrate/presence/install/bassclef-sync.template.sh +0 -2076
  203. package/substrate/presence/install/schedule-auto-save.cron.sh +0 -88
  204. package/substrate/presence/install/schedule-auto-save.taskscheduler.md +0 -122
  205. package/substrate/scripts/aggregate-telemetry.sh +0 -217
  206. package/substrate/scripts/analyze-tier-dependencies.sh +0 -239
  207. package/substrate/scripts/generate-lite-manifest.sh +0 -505
  208. package/substrate/scripts/generate-tier-manifest.sh +0 -28
  209. package/substrate/scripts/intent-drift-check.sh +0 -456
  210. package/substrate/scripts/lite-manifest-drift-check.sh +0 -146
  211. package/substrate/scripts/render-lite-manifest-doc.sh +0 -150
  212. package/substrate/standards/adr-template.md +0 -86
  213. package/substrate/standards/api-conventions/nextjs.md +0 -84
  214. package/substrate/standards/artifact-composition.md +0 -209
  215. package/substrate/standards/bash-hook-safety.md +0 -246
  216. package/substrate/standards/bassclef-configs-schema.md +0 -232
  217. package/substrate/standards/bassclef-evolution.md +0 -143
  218. package/substrate/standards/bassclef-internal-jargon.md +0 -244
  219. package/substrate/standards/bassclef-managed-sentinel.md +0 -96
  220. package/substrate/standards/bassclef-source-config.md +0 -228
  221. package/substrate/standards/branch-stacking.md +0 -408
  222. package/substrate/standards/code-safety-principles.md +0 -176
  223. package/substrate/standards/composer-prerequisites.md +0 -155
  224. package/substrate/standards/deferred-actions-schema.md +0 -204
  225. package/substrate/standards/dependency-discipline/cargo.md +0 -39
  226. package/substrate/standards/dependency-discipline/gem.md +0 -43
  227. package/substrate/standards/dependency-discipline/go-mod.md +0 -41
  228. package/substrate/standards/dependency-discipline/npm.md +0 -42
  229. package/substrate/standards/dependency-discipline/pip.md +0 -42
  230. package/substrate/standards/deployment-topology/ec2-tailscale.md +0 -225
  231. package/substrate/standards/deployment-topology.md +0 -69
  232. package/substrate/standards/docs-sync-allowlist.md +0 -76
  233. package/substrate/standards/domain-and-dns.md +0 -145
  234. package/substrate/standards/frontend-stack.md +0 -67
  235. package/substrate/standards/frontmatter-schema.md +0 -154
  236. package/substrate/standards/graceful-exit.md +0 -227
  237. package/substrate/standards/hook-idempotency.md +0 -102
  238. package/substrate/standards/hook-injection-discipline.md +0 -202
  239. package/substrate/standards/hook-install-class.md +0 -215
  240. package/substrate/standards/input-handler-interface.md +0 -152
  241. package/substrate/standards/lite-manifest-schema-changes.md +0 -135
  242. package/substrate/standards/luminary-matching.md +0 -105
  243. package/substrate/standards/luminary-problem-patterns.md +0 -481
  244. package/substrate/standards/migration-discipline/active-record.md +0 -50
  245. package/substrate/standards/migration-discipline/alembic.md +0 -43
  246. package/substrate/standards/migration-discipline/gorm.md +0 -50
  247. package/substrate/standards/migration-discipline/prisma.md +0 -53
  248. package/substrate/standards/migration-discipline/sqlalchemy.md +0 -51
  249. package/substrate/standards/mobile-ephemeral-session.md +0 -167
  250. package/substrate/standards/model-routing-discipline.md +0 -160
  251. package/substrate/standards/ogilvy-writing-rules.md +0 -225
  252. package/substrate/standards/opener-discipline.md +0 -96
  253. package/substrate/standards/operator-facing-prose-discipline.md +0 -201
  254. package/substrate/standards/persona-schema.md +0 -229
  255. package/substrate/standards/pluggable-luminaries.md +0 -323
  256. package/substrate/standards/pr-body-discipline.md +0 -115
  257. package/substrate/standards/preview-state-schema.md +0 -189
  258. package/substrate/standards/project-directory-layout.md +0 -276
  259. package/substrate/standards/release-tagging.md +0 -137
  260. package/substrate/standards/reserved-skill-names.md +0 -120
  261. package/substrate/standards/scannable-multi-option-output.md +0 -261
  262. package/substrate/standards/sdlc-compliance.md +0 -286
  263. package/substrate/standards/sdlc-gates/typescript.md +0 -57
  264. package/substrate/standards/secrets-lifecycle.md +0 -210
  265. package/substrate/standards/security-scanner-adapter.md +0 -145
  266. package/substrate/standards/session-board.md +0 -256
  267. package/substrate/standards/skill-output-discipline.md +0 -90
  268. package/substrate/standards/state-spine-contract.md +0 -255
  269. package/substrate/standards/state-spine.md +0 -511
  270. package/substrate/standards/steering-hints/kiss-words.md +0 -11
  271. package/substrate/standards/substrate-config-schema.md +0 -267
  272. package/substrate/standards/tech-stack-config.md +0 -109
  273. package/substrate/standards/tier-dependency-analysis.md +0 -167
  274. package/substrate/standards/tier-runtime-deps/lite.md +0 -57
  275. package/substrate/standards/tier-tag-schema.md +0 -155
  276. package/substrate/standards/two-layer-config.md +0 -99
  277. package/substrate/standards/use-case-format.md +0 -292
  278. package/substrate/standards/user-story-invest.md +0 -268
  279. package/substrate/standards/velocity-and-appetite.md +0 -229
  280. package/substrate/standards/voice-input-pattern.md +0 -119
  281. package/substrate/standards/whereami-schema.md +0 -301
  282. package/substrate/standards/worktree-management.md +0 -211
  283. package/substrate/standards/writing-guide.md +0 -213
  284. package/substrate/templates/chronicle-template.md +0 -75
  285. package/substrate/templates/deferred-action-template.md +0 -45
  286. package/substrate/templates/memory-proposal-template.md +0 -77
  287. package/substrate/templates/persona-template.md +0 -200
  288. package/substrate/templates/pr-faq.md +0 -45
  289. package/substrate/templates/secret-rotation-template.md +0 -162
  290. package/substrate/templates/spec-template.md +0 -131
  291. package/substrate/templates/use-case-template.md +0 -194
  292. package/substrate/templates/user-story-template.md +0 -107
  293. package/substrate/templates/whereami-template.md +0 -101
@@ -1,1088 +0,0 @@
1
- #!/usr/bin/env bash
2
- # tier: lite
3
- # lib/tier-dependency-audit.sh
4
- #
5
- # Sourced library for the tier-dependency-audit subsystem. Ships the 8
6
- # Larman/GRASP responsibilities named in
7
- # docs/decompositions/2026-08-17a-tier-dependency-audit.md Phase 1.
8
- #
9
- # Per D1 — extracted from scripts/analyze-tier-dependencies.sh for
10
- # per-function testability + reuse across tier consumers (lite, standard,
11
- # standard-pro, ultra, upstream).
12
- #
13
- # Per D3 — REFERENCE_KINDS lookup table replaces hardcoded reference kinds.
14
- # Per D4 — reader functions log per-source counts to stderr.
15
- #
16
- # Anchor luminaries:
17
- # @luminary david-parnas — information hiding; each function hides one
18
- # design decision behind a stable contract
19
- # @luminary john-ousterhout — deep modules; small interface + hidden work
20
- # @luminary michael-feathers — characterization tests per function
21
- # (see .claude/hooks/tests/lib-tier-dependency-audit.test.sh)
22
- #
23
- # Consumers source this file:
24
- # source lib/tier-dependency-audit.sh
25
- #
26
- # Tests: .claude/hooks/tests/lib-tier-dependency-audit.test.sh (48 tests as of goal 08-17d)
27
- # Standard: standards/tier-dependency-analysis.md
28
-
29
- # Repo root derived from lib file location — stable across callers' CWDs.
30
- # Used by read_runtime_deps for glob expansion via `git -C`.
31
- _TIER_AUDIT_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
32
- _TIER_AUDIT_REPO_ROOT="$(cd "$_TIER_AUDIT_LIB_DIR/.." && pwd)"
33
-
34
- # ---------------------------------------------------------------------------
35
- # REFERENCE_KINDS — the 7 reference kinds the subsystem walks
36
- # Per D3. Each entry: "kind|grep-pattern|extraction-fn"
37
- # Extension: add a new line + implement the extraction fn below.
38
- # ---------------------------------------------------------------------------
39
- REFERENCE_KINDS=(
40
- "composes_with|^composes_with:|_extract_composes_with"
41
- "source_lib|^[[:space:]]*source[[:space:]]+lib/|_extract_source_lib"
42
- "additional_directories|additionalDirectories|_extract_additional_directories"
43
- "bash_scripts|bash[[:space:]]+scripts/|_extract_bash_scripts"
44
- "luminary_ref|@luminary[[:space:]]+|_extract_luminary_ref"
45
- "standards_ref|standards/[a-zA-Z0-9._/-]+\\.md|_extract_standards_ref"
46
- "rules_ref|\\.claude/rules/[a-zA-Z0-9._/-]+\\.md|_extract_rules_ref"
47
- "jsonc_config|bassclef-configs\\.jsonc|_extract_jsonc_config"
48
- "hook_ref|\\.claude/hooks/[a-zA-Z0-9._/-]+\\.sh|_extract_hook_ref"
49
- "marker_ref|state/markers/[a-zA-Z0-9._/-]+\\.marker|_extract_marker_ref"
50
- "state_file_read|state/[a-zA-Z0-9._/-]+\\.json|_extract_state_file_read"
51
- "template_ref|templates/[a-zA-Z0-9._/-]+\\.md_or_yml|_extract_template_ref"
52
- "python_script|scripts/[a-zA-Z0-9._/-]+\\.py|_extract_python_script"
53
- "allowlist_ref|\\.claude/hooks/[a-zA-Z0-9._/-]*-allowlist\\.txt|_extract_allowlist_ref"
54
- )
55
-
56
- # ---------------------------------------------------------------------------
57
- # parse_tier_arg — Validate tier arg, return normalized value
58
- # Interface: ITierArgParser (Larman)
59
- # Inputs: $1 = tier string
60
- # Outputs: stdout normalized tier; stderr usage on error
61
- # Exit: 0 valid, 2 invalid or missing
62
- # ---------------------------------------------------------------------------
63
- parse_tier_arg() {
64
- local tier="${1:-}"
65
- if [ -z "$tier" ]; then
66
- echo "Usage: parse_tier_arg <tier>" >&2
67
- echo " tier: one of lite | standard | standard-pro | ultra | upstream" >&2
68
- return 2
69
- fi
70
- case "$tier" in
71
- lite|standard|standard-pro|ultra|upstream)
72
- echo "$tier"
73
- return 0
74
- ;;
75
- *)
76
- echo "parse_tier_arg: invalid tier '$tier'" >&2
77
- echo " expected one of lite | standard | standard-pro | ultra | upstream" >&2
78
- return 2
79
- ;;
80
- esac
81
- }
82
-
83
- # ---------------------------------------------------------------------------
84
- # parse_tier_list_arg — Parse combined-tier arg into a list
85
- # Per ADR-052 D5. Accepts "all" (expands to 5 tiers) OR a comma list
86
- # (e.g., "lite,standard"). Single tier also accepted for uniform callers.
87
- # Interface: ITierListParser (Larman)
88
- # Inputs: $1 = arg string (all | tier | tier,tier[,...])
89
- # Outputs: stdout: one tier per line; stderr usage on error
90
- # Exit: 0 valid, 2 invalid or missing (empty, unknown tier, or mixed "all,X")
91
- # ---------------------------------------------------------------------------
92
- parse_tier_list_arg() {
93
- local arg="${1:-}"
94
- if [ -z "$arg" ]; then
95
- echo "Usage: parse_tier_list_arg <arg>" >&2
96
- echo " arg: 'all' | <tier> | <tier>,<tier>[,...]" >&2
97
- echo " tier: one of lite | standard | standard-pro | ultra | upstream" >&2
98
- return 2
99
- fi
100
- # 'all' expands to the 5 canonical tiers; must be alone, not mixed.
101
- if [ "$arg" = "all" ]; then
102
- printf "lite\nstandard\nstandard-pro\nultra\nupstream\n"
103
- return 0
104
- fi
105
- # Reject mixed "all,X" or "X,all"
106
- case ",$arg," in
107
- *,all,*)
108
- echo "parse_tier_list_arg: 'all' cannot be mixed with tier names" >&2
109
- return 2
110
- ;;
111
- esac
112
- # Split on comma; validate each; dedupe via sort -u
113
- local tmp
114
- tmp="$(mktemp)"
115
- local item
116
- local IFS=','
117
- # shellcheck disable=SC2086
118
- for item in $arg; do
119
- case "$item" in
120
- lite|standard|standard-pro|ultra|upstream)
121
- echo "$item" >> "$tmp"
122
- ;;
123
- *)
124
- echo "parse_tier_list_arg: invalid tier '$item'" >&2
125
- echo " expected one of lite | standard | standard-pro | ultra | upstream" >&2
126
- rm -f "$tmp"
127
- return 2
128
- ;;
129
- esac
130
- done
131
- sort -u "$tmp"
132
- rm -f "$tmp"
133
- return 0
134
- }
135
-
136
- # ---------------------------------------------------------------------------
137
- # read_intended_source_1 — files carrying tier: <tier> frontmatter or header
138
- # Interface: IIntendedSourceReader (Larman)
139
- # Inputs: $1 = tier
140
- # Outputs: stdout: one path per line, sorted-unique
141
- # stderr: "Source 1: <N> files" per D4 observability
142
- # Exit: 0
143
- # ---------------------------------------------------------------------------
144
- read_intended_source_1() {
145
- local tier="$1"
146
- local out
147
- out="$({
148
- git grep -l "^tier: ${tier}$" -- '*.md' 2>/dev/null || true
149
- git grep -l "^# tier: ${tier}$" -- '*.sh' 2>/dev/null || true
150
- } | sort -u)"
151
- local count
152
- count="$(echo "$out" | grep -c . 2>/dev/null || echo 0)"
153
- echo "Source 1 (tagged): $count files" >&2
154
- if [ -n "$out" ]; then
155
- echo "$out"
156
- fi
157
- return 0
158
- }
159
-
160
- # ---------------------------------------------------------------------------
161
- # extract_references — parse references from a file
162
- # Interface: IReferenceExtractor (Larman); Strategy per D3
163
- # Inputs: $1 = file path
164
- # Outputs: stdout: one target path per line
165
- # Exit: 0 (missing file returns empty, no error)
166
- # ---------------------------------------------------------------------------
167
- extract_references() {
168
- local f="$1"
169
- [ -f "$f" ] || return 0
170
- local entry kind pattern extractor
171
- for entry in "${REFERENCE_KINDS[@]}"; do
172
- IFS='|' read -r kind pattern extractor <<< "$entry"
173
- "$extractor" "$f" 2>/dev/null || true
174
- done
175
- }
176
-
177
- # Reference extractors (one per REFERENCE_KIND row)
178
- _extract_composes_with() {
179
- local f="$1"
180
- # composes_with: [alpha, beta] OR composes_with: alpha
181
- awk '
182
- /^composes_with:/ {
183
- line=$0
184
- sub(/^composes_with:[[:space:]]*\[?/, "", line)
185
- sub(/\].*$/, "", line)
186
- gsub(/[[:space:]]/, "", line)
187
- n = split(line, arr, ",")
188
- for (i = 1; i <= n; i++) if (arr[i] != "") print ".claude/skills/" arr[i] "/SKILL.md"
189
- }
190
- ' "$f"
191
- }
192
-
193
- _extract_source_lib() {
194
- local f="$1"
195
- # grep -oE is more portable than BSD awk match() for this pattern
196
- grep -oE 'lib/[a-zA-Z0-9._/-]+\.sh' "$f" 2>/dev/null | sort -u
197
- }
198
-
199
- _extract_additional_directories() {
200
- local f="$1"
201
- awk '
202
- /additionalDirectories/ {
203
- # Best-effort — grep the values on the same or nearby lines
204
- match($0, /"[^"]+"/)
205
- while (RSTART > 0) {
206
- val = substr($0, RSTART + 1, RLENGTH - 2)
207
- if (val ~ /\//) print val
208
- s = substr($0, RSTART + RLENGTH)
209
- $0 = s
210
- match($0, /"[^"]+"/)
211
- }
212
- }
213
- ' "$f"
214
- }
215
-
216
- _extract_bash_scripts() {
217
- local f="$1"
218
- # G1 fix (goal 2026-08-21b): BSD awk match+substr unreliable on macOS
219
- # per memory feedback_grep_oE_beats_bsd_awk_match_for_portability.
220
- # Replace with grep -oE + sed prefix strip; keeps the "bash scripts/*"
221
- # invocation constraint (mere mention of scripts/foo.sh in prose is
222
- # NOT a runtime edge; the "bash " prefix marks actual invocation).
223
- grep -oE 'bash[[:space:]]+scripts/[a-zA-Z0-9._/-]+\.sh' "$f" 2>/dev/null | \
224
- sed -E 's/^bash[[:space:]]+//' | sort -u
225
- }
226
-
227
- _extract_luminary_ref() {
228
- local f="$1"
229
- awk '
230
- {
231
- s = $0
232
- while (match(s, /@luminary[[:space:]]+[a-z][a-z0-9-]+/)) {
233
- val = substr(s, RSTART, RLENGTH)
234
- sub(/@luminary[[:space:]]+/, "", val)
235
- print ".claude/luminaries/" val ".md"
236
- s = substr(s, RSTART + RLENGTH)
237
- }
238
- }
239
- ' "$f"
240
- }
241
-
242
- _extract_standards_ref() {
243
- local f="$1"
244
- # grep -oE more portable than BSD awk match() loops
245
- grep -oE 'standards/[a-zA-Z0-9._/-]+\.md' "$f" 2>/dev/null | sort -u
246
- }
247
-
248
- _extract_rules_ref() {
249
- local f="$1"
250
- grep -oE '\.claude/rules/[a-zA-Z0-9._/-]+\.md' "$f" 2>/dev/null | sort -u
251
- }
252
-
253
- # G4a extractor (goal 2026-08-21b Step 3): JSONC config reads
254
- # Matches `.claude/bassclef-configs.jsonc` OR bare `bassclef-configs.jsonc`;
255
- # normalizes both to the canonical substrate path so the walk finds the
256
- # same target regardless of how the caller wrote it.
257
- _extract_jsonc_config() {
258
- local f="$1"
259
- grep -oE '(\.claude/)?bassclef-configs\.jsonc' "$f" 2>/dev/null | \
260
- sed -E 's|^bassclef-configs\.jsonc$|.claude/bassclef-configs.jsonc|' | \
261
- sort -u
262
- }
263
-
264
- # G4b extractor (goal 2026-08-21b Step 4): hook references
265
- # Matches `.claude/hooks/<path>.sh` anywhere in a file. Primary use case:
266
- # YAML workflow refs (GitHub Actions call hooks via bash + shell steps),
267
- # but also catches doc mentions + settings.json wiring references.
268
- _extract_hook_ref() {
269
- local f="$1"
270
- grep -oE '\.claude/hooks/[a-zA-Z0-9._/-]+\.sh' "$f" 2>/dev/null | sort -u
271
- }
272
-
273
- # G4d extractors (goal 2026-08-21b Step 5): marker + state file reads
274
- # Marker edges: hooks read state/markers/**/*.marker to gate behavior
275
- # (temperance markers, luminary markers, etc). State-file edges: hooks
276
- # read state/*.json (state-spine entities like whereami.json, session
277
- # markers). Adopter runtime state edge class per gap analysis G4d.
278
- _extract_marker_ref() {
279
- local f="$1"
280
- grep -oE 'state/markers/[a-zA-Z0-9._/-]+\.marker' "$f" 2>/dev/null | sort -u
281
- }
282
-
283
- _extract_state_file_read() {
284
- local f="$1"
285
- grep -oE 'state/[a-zA-Z0-9._/-]+\.json' "$f" 2>/dev/null | sort -u
286
- }
287
-
288
- # G4c extractor (goal 2026-08-22a Step 1; ticket #1353): template references
289
- # Matches `templates/<name>.<ext>` where ext is md/yml/yaml/json. Templates
290
- # live at repo root under templates/ per `ls templates/` (21 files as of
291
- # 2026-08-22). Same shape as _extract_hook_ref pattern per G4b (PR #1347).
292
- _extract_template_ref() {
293
- local f="$1"
294
- grep -oE 'templates/[a-zA-Z0-9._/-]+\.(md|yml|yaml|json)' "$f" 2>/dev/null | sort -u
295
- }
296
-
297
- # G4e extractor (goal 2026-08-22a Step 2; ticket #1354): Python script refs
298
- # Matches `scripts/<name>.py` anywhere. Covers bare mentions, `python3
299
- # scripts/foo.py` invocations, and cross-refs from docs. ~30 Python
300
- # scripts live under scripts/ per `ls scripts/*.py`. Same shape as G4b.
301
- _extract_python_script() {
302
- local f="$1"
303
- grep -oE 'scripts/[a-zA-Z0-9._/-]+\.py' "$f" 2>/dev/null | sort -u
304
- }
305
-
306
- # G4f extractor (goal 2026-08-22a Step 3; ticket #1355): allowlist txt refs
307
- # Matches `.claude/hooks/<...>-allowlist.txt`. 10 allowlist files exist
308
- # under .claude/hooks/. Pattern requires the `-allowlist.txt` suffix so
309
- # we do not match plain .txt files (docs, fixtures). Same grep shape as
310
- # G4b hook-ref.
311
- _extract_allowlist_ref() {
312
- local f="$1"
313
- grep -oE '\.claude/hooks/[a-zA-Z0-9._/-]*-allowlist\.txt' "$f" 2>/dev/null | sort -u
314
- }
315
-
316
- # C0 circuit-breaker (goal 2026-08-22a Step 4; ticket #1356): detect
317
- # reference-shaped strings in a file that no registered extractor caught.
318
- # Silent-skip cure — the walker cannot tell "no reference exists" from
319
- # "extractor missing". This function surfaces the gap.
320
- #
321
- # V1 narrow scope: function + Tier 0 tests only. Runner + CI hook wire
322
- # deferred to Phase 2 (follow-on ticket).
323
- #
324
- # Candidate pattern matches any path-shaped string with a file extension:
325
- # starts with letter or dot, has one or more path parts joined by /, ends
326
- # with .<lowercase-ext>. Then diffs against what registered extractors
327
- # returned via extract_references(). Difference = unhandled.
328
- #
329
- # Per @luminary michael-feathers — characterization test for the walker's
330
- # coverage gap. Per @luminary saltzer-schroeder — fail-safe: silent skip
331
- # becomes surfaced signal.
332
- _detect_unhandled_references() {
333
- local f="$1"
334
- [ -f "$f" ] || return 0
335
- local candidates handled
336
- candidates="$(grep -oE '[.a-zA-Z][.a-zA-Z0-9_-]*/[.a-zA-Z0-9_/-]+\.[a-z]+' "$f" 2>/dev/null | sort -u)"
337
- [ -z "$candidates" ] && return 0
338
- handled="$(extract_references "$f" 2>/dev/null | sort -u)"
339
- comm -23 <(echo "$candidates") <(echo "$handled")
340
- }
341
-
342
- # ---------------------------------------------------------------------------
343
- # walk_graph_bfs — BFS from seed set using an extractor function
344
- # Interface: IGraphWalker (Larman)
345
- # Inputs: $1 = seeds file, $2 = extractor function name
346
- # Outputs: stdout: reachable paths, sorted-unique
347
- # Exit: 0
348
- # Cycle protection: visited set prevents re-processing
349
- # ---------------------------------------------------------------------------
350
- walk_graph_bfs() {
351
- local seeds_file="$1"
352
- local extractor="$2"
353
- local visited_file queue_file
354
- visited_file="$(mktemp)"
355
- queue_file="$(mktemp)"
356
- trap 'rm -f "$visited_file" "$queue_file"' RETURN
357
-
358
- # Seed the queue
359
- if [ -s "$seeds_file" ]; then
360
- cp "$seeds_file" "$queue_file"
361
- fi
362
-
363
- local max_iter=1000 iter=0
364
- while [ -s "$queue_file" ] && [ "$iter" -lt "$max_iter" ]; do
365
- iter=$((iter + 1))
366
- local next_file; next_file="$(mktemp)"
367
- while IFS= read -r path; do
368
- [ -z "$path" ] && continue
369
- # Skip if visited
370
- if grep -Fxq "$path" "$visited_file" 2>/dev/null; then
371
- continue
372
- fi
373
- echo "$path" >> "$visited_file"
374
- # Extract references from this file
375
- "$extractor" "$path" 2>/dev/null | while IFS= read -r ref; do
376
- [ -n "$ref" ] && echo "$ref" >> "$next_file"
377
- done
378
- done < "$queue_file"
379
- mv "$next_file" "$queue_file"
380
- sort -u -o "$queue_file" "$queue_file"
381
- done
382
-
383
- sort -u "$visited_file"
384
- return 0
385
- }
386
-
387
- # ---------------------------------------------------------------------------
388
- # read_runtime_deps — parse tier-runtime-deps fenced block, expand globs
389
- # Interface: IRuntimeDepsReader (Larman)
390
- # Per goal 08-17d Session 1b Step 2.5 — extended with glob expansion so a
391
- # single pattern (e.g., state/luminary-implementations/*.json) covers auto-
392
- # generated file sets the graph walk cannot reach.
393
- # Inputs: $1 = runtime-deps file path
394
- # Outputs: stdout: one path per line (globs expanded relative to repo root
395
- # via git ls-files; literal paths passed through)
396
- # stderr: "Source 3: <N> files" per D4 observability
397
- # Exit: 0 pass, 3 missing file
398
- # ---------------------------------------------------------------------------
399
- read_runtime_deps() {
400
- local f="$1"
401
- if [ ! -f "$f" ]; then
402
- echo "read_runtime_deps: runtime-deps file not found at $f" >&2
403
- return 3
404
- fi
405
- # Parse the fenced block, expand each line
406
- local expanded
407
- expanded="$(
408
- awk '
409
- /^```/ { in_block = !in_block; next }
410
- in_block && !/^[[:space:]]*(#|$)/ { print }
411
- ' "$f" | while IFS= read -r line; do
412
- [ -z "$line" ] && continue
413
- if [[ "$line" == *"*"* ]]; then
414
- # Glob — expand via git ls-files (respects .gitignore)
415
- git -C "$_TIER_AUDIT_REPO_ROOT" ls-files -- "$line" 2>/dev/null || true
416
- else
417
- printf "%s\n" "$line"
418
- fi
419
- done
420
- )"
421
- local count
422
- count="$(echo "$expanded" | grep -c . 2>/dev/null || echo 0)"
423
- echo "Source 3 (runtime deps): $count files" >&2
424
- if [ -n "$expanded" ]; then
425
- echo "$expanded"
426
- fi
427
- return 0
428
- }
429
-
430
- # ---------------------------------------------------------------------------
431
- # read_actual_source — invoke release-script dry-run + parse include list
432
- # Interface: IActualSourceReader (Larman); Adapter for external cmd
433
- # Inputs: $1 = tier
434
- # Outputs: stdout: release-script include list, sorted-unique
435
- # stderr: "Source 4: <N> files" per D4 observability
436
- # Exit: 0 pass, 4 release script failure
437
- # ---------------------------------------------------------------------------
438
- read_actual_source() {
439
- local tier="$1"
440
- local release_script
441
- release_script="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/scripts/release-to-bassclef.sh"
442
- if [ ! -x "$release_script" ]; then
443
- echo "read_actual_source: release script not found or not executable at $release_script" >&2
444
- return 4
445
- fi
446
- # Real-mode: invoke dry-run with INCLUDE_LIST_OUT hook.
447
- # The release script writes the include list to $INCLUDE_LIST_OUT after
448
- # building it, before its BLOCK gates fire (sync-delete threshold, andon).
449
- # This means we get the include list even when the release would fail
450
- # for reasons unrelated to file inclusion — exactly the case for tier audits
451
- # (we WANT to know what's shipping even if release is blocked).
452
- # Bypasses: SKIP_PRIOR_PR_CHECK (audit doesn't care about prior PRs)
453
- # SYNC_DELETE=0 (audit computes bucket 3 itself)
454
- local include_out
455
- include_out="$(mktemp)"
456
- INCLUDE_LIST_OUT="$include_out" \
457
- SKIP_PRIOR_PR_CHECK=1 SYNC_DELETE=0 \
458
- "$release_script" --dry-run --supersede >/dev/null 2>&1 || true
459
- # Even if the release script exits non-zero (BLOCKed at andon or tag-exists),
460
- # INCLUDE_LIST_OUT should be populated if the pipeline reached the file-list build.
461
- if [ ! -s "$include_out" ]; then
462
- echo "read_actual_source: release script did not produce include list at $include_out" >&2
463
- rm -f "$include_out"
464
- return 4
465
- fi
466
- local out; out="$(sort -u "$include_out")"
467
- rm -f "$include_out"
468
- local count
469
- count="$(echo "$out" | grep -c . 2>/dev/null || echo 0)"
470
- echo "Source 4 (release include): $count files" >&2
471
- if [ -n "$out" ]; then
472
- echo "$out"
473
- fi
474
- return 0
475
- }
476
-
477
- # ---------------------------------------------------------------------------
478
- # classify_files — assign each file to bucket 1-4
479
- # Interface: IClassifier (Larman); pure function
480
- # Inputs: $1 = intended file, $2 = actual file, $3 = tags file (path\ttier),
481
- # $4 = out-dir, $5 = tier
482
- # Outputs: writes 4 bucket files to out-dir
483
- # Exit: 0
484
- # ---------------------------------------------------------------------------
485
- classify_files() {
486
- local intended="$1" actual="$2" tags="$3" out_dir="$4" tier="$5"
487
- mkdir -p "$out_dir"
488
- local bucket_1="$out_dir/bucket-1.txt"
489
- local bucket_2a="$out_dir/bucket-2a.txt"
490
- local bucket_2b="$out_dir/bucket-2b.txt"
491
- local bucket_3="$out_dir/bucket-3.txt"
492
- local bucket_4="$out_dir/bucket-4.txt"
493
- : > "$bucket_1"; : > "$bucket_2a"; : > "$bucket_2b"
494
- : > "$bucket_3"; : > "$bucket_4"
495
-
496
- # Sort inputs for comm
497
- local intended_sorted actual_sorted
498
- intended_sorted="$(mktemp)"; actual_sorted="$(mktemp)"
499
- sort -u "$intended" > "$intended_sorted"
500
- sort -u "$actual" > "$actual_sorted"
501
-
502
- # Bucket 1 + Bucket 2a + Bucket 2b — split intersection by tag state.
503
- # Per ADR-043 D1 + docs/canvases/2026-07-19-bassclef-lite.md L76:
504
- # tag == target tier → bucket 1 (ships correctly in this tier)
505
- # no tag at all → bucket 2a (safe mechanical tag)
506
- # tag == other tier → bucket 2b (chain-snap risk; per-file cure)
507
- comm -12 "$intended_sorted" "$actual_sorted" | while IFS= read -r path; do
508
- # Look up tag for path in tags file. Returns "" for untagged (no tab)
509
- # or missing entry; returns tier value when tagged.
510
- local file_tag
511
- file_tag="$(awk -F'\t' -v p="$path" '$1 == p { print ($2 ? $2 : ""); exit }' "$tags")"
512
- if [ "$file_tag" = "$tier" ]; then
513
- echo "$path" >> "$bucket_1"
514
- elif [ -z "$file_tag" ]; then
515
- echo "$path" >> "$bucket_2a"
516
- else
517
- echo "$path" >> "$bucket_2b"
518
- fi
519
- done
520
-
521
- # Bucket 3 — ACTUAL \ INTENDED
522
- comm -23 "$actual_sorted" "$intended_sorted" > "$bucket_3"
523
-
524
- # Bucket 4 — INTENDED \ ACTUAL
525
- comm -13 "$actual_sorted" "$intended_sorted" > "$bucket_4"
526
-
527
- rm -f "$intended_sorted" "$actual_sorted"
528
- return 0
529
- }
530
-
531
- # ---------------------------------------------------------------------------
532
- # classify_files_combined — assign each file to bucket 1-4 under combined-tier
533
- # Per ADR-052 D5. INTENDED is the union of all named tiers' INTENDED sets.
534
- # The 2a/2b split disappears — combined mode does not distinguish "tagged for
535
- # a different tier" because in combined mode, that tag IS the file's home in
536
- # the union.
537
- # Interface: IClassifierCombined (Larman); pure function
538
- # Inputs: $1 = intended file (union across tiers), $2 = actual file,
539
- # $3 = tags file (path\ttier), $4 = out-dir
540
- # Outputs: writes 4 bucket files to out-dir (bucket-1, bucket-2, bucket-3, bucket-4)
541
- # Exit: 0
542
- # ---------------------------------------------------------------------------
543
- classify_files_combined() {
544
- local intended="$1" actual="$2" tags="$3" out_dir="$4"
545
- mkdir -p "$out_dir"
546
- local bucket_1="$out_dir/bucket-1.txt"
547
- local bucket_2="$out_dir/bucket-2.txt"
548
- local bucket_3="$out_dir/bucket-3.txt"
549
- local bucket_4="$out_dir/bucket-4.txt"
550
- : > "$bucket_1"; : > "$bucket_2"
551
- : > "$bucket_3"; : > "$bucket_4"
552
-
553
- local intended_sorted actual_sorted
554
- intended_sorted="$(mktemp)"; actual_sorted="$(mktemp)"
555
- sort -u "$intended" > "$intended_sorted"
556
- sort -u "$actual" > "$actual_sorted"
557
-
558
- # Bucket 1 + Bucket 2 — split intersection by tag presence.
559
- # In combined mode, ANY non-empty tag places the file in bucket 1.
560
- comm -12 "$intended_sorted" "$actual_sorted" | while IFS= read -r path; do
561
- local file_tag
562
- file_tag="$(awk -F'\t' -v p="$path" '$1 == p { print ($2 ? $2 : ""); exit }' "$tags")"
563
- if [ -n "$file_tag" ]; then
564
- echo "$path" >> "$bucket_1"
565
- else
566
- echo "$path" >> "$bucket_2"
567
- fi
568
- done
569
-
570
- # Bucket 3 — ACTUAL \ INTENDED_ALL (true accidental leaks)
571
- comm -23 "$actual_sorted" "$intended_sorted" > "$bucket_3"
572
-
573
- # Bucket 4 — INTENDED_ALL \ ACTUAL (missing from release)
574
- comm -13 "$actual_sorted" "$intended_sorted" > "$bucket_4"
575
-
576
- rm -f "$intended_sorted" "$actual_sorted"
577
- return 0
578
- }
579
-
580
- # ---------------------------------------------------------------------------
581
- # write_manifest — emit markdown from bucket files
582
- # Interface: IManifestWriter (Larman)
583
- # Inputs: $1 = bucket dir (contains bucket-N.txt), $2 = date, $3 = tier,
584
- # $4 = output path
585
- # Outputs: writes manifest file at $4
586
- # Exit: 0 pass, 5 dir not writable
587
- # ---------------------------------------------------------------------------
588
- write_manifest() {
589
- local bucket_dir="$1" date="$2" tier="$3" outfile="$4"
590
- local out_parent; out_parent="$(dirname "$outfile")"
591
- mkdir -p "$out_parent" 2>/dev/null || { echo "write_manifest: cannot create $out_parent" >&2; return 5; }
592
- local c1 c2a c2b c3 c4
593
- c1="$(wc -l < "$bucket_dir/bucket-1.txt" 2>/dev/null | tr -d ' ')"
594
- c2a="$(wc -l < "$bucket_dir/bucket-2a.txt" 2>/dev/null | tr -d ' ')"
595
- c2b="$(wc -l < "$bucket_dir/bucket-2b.txt" 2>/dev/null | tr -d ' ')"
596
- c3="$(wc -l < "$bucket_dir/bucket-3.txt" 2>/dev/null | tr -d ' ')"
597
- c4="$(wc -l < "$bucket_dir/bucket-4.txt" 2>/dev/null | tr -d ' ')"
598
- {
599
- echo "---"
600
- echo "tier_audit: ${tier}"
601
- echo "generated_at: ${date}"
602
- echo "---"
603
- echo ""
604
- echo "# Tier-dependency audit — ${tier} — ${date}"
605
- echo ""
606
- echo "Per standards/tier-dependency-analysis.md. Five-bucket classification (Bucket 2 split into 2a + 2b per ADR-043 D1 + canvas 2026-07-19-bassclef-lite.md L76)."
607
- echo ""
608
- echo "## Summary"
609
- echo ""
610
- echo "Bucket 1 (Tagged; ships correctly): ${c1:-0}"
611
- echo "Bucket 2a (No tag; safe mechanical tag): ${c2a:-0}"
612
- echo "Bucket 2b (Chain-snap risk; tagged non-${tier}; needs per-file cure): ${c2b:-0}"
613
- echo "Bucket 3 (Accidental leak; needs removal): ${c3:-0}"
614
- echo "Bucket 4 (Missing from release; needs release fix): ${c4:-0}"
615
- echo ""
616
- for i in 1 2a 2b 3 4; do
617
- local label file
618
- file="$bucket_dir/bucket-${i}.txt"
619
- case "$i" in
620
- 1) label="Bucket 1 — Tagged (ships correctly)" ;;
621
- 2a) label="Bucket 2a — Missing tag (no tier tag; safe mechanical add)" ;;
622
- 2b) label="Bucket 2b — Chain-snap risk (tagged non-${tier}; per-file cure per canvas Q1+Q2)" ;;
623
- 3) label="Bucket 3 — Accidental leak (in release but not in graph)" ;;
624
- 4) label="Bucket 4 — Missing from release (in graph but not in release)" ;;
625
- esac
626
- echo "## ${label}"
627
- echo ""
628
- if [ -s "$file" ]; then
629
- sort "$file" | sed 's/^/- /'
630
- else
631
- echo "_(empty)_"
632
- fi
633
- echo ""
634
- done
635
- echo "## Recommended fix order"
636
- echo ""
637
- echo "1. Fix Bucket 4 first (release script gap; may hide legit ships)"
638
- echo "2. Fix Bucket 2a (mechanical tag; safe helper at scripts/apply-bucket2-tier-tags.sh)"
639
- echo "3. Review Bucket 2b (per-file: promote to ${tier}, inline into caller, or determine graph over-included)"
640
- echo "4. Fix Bucket 3 (remove accidental leaks + extend .gitignore)"
641
- } > "$outfile"
642
- return 0
643
- }
644
-
645
- # ---------------------------------------------------------------------------
646
- # write_manifest_combined — emit markdown for combined-tier audit
647
- # Per ADR-052 D5. 4 buckets (no 2a/2b split); header names the tier list.
648
- # Interface: IManifestWriterCombined (Larman)
649
- # Inputs: $1 = bucket dir (contains bucket-1.txt, bucket-2.txt, bucket-3.txt, bucket-4.txt),
650
- # $2 = date, $3 = tier list string (e.g., "lite,standard" or "all"),
651
- # $4 = output path
652
- # Outputs: writes manifest file at $4
653
- # Exit: 0 pass, 5 dir not writable
654
- # ---------------------------------------------------------------------------
655
- write_manifest_combined() {
656
- local bucket_dir="$1" date="$2" tier_list="$3" outfile="$4"
657
- local out_parent; out_parent="$(dirname "$outfile")"
658
- mkdir -p "$out_parent" 2>/dev/null || { echo "write_manifest_combined: cannot create $out_parent" >&2; return 5; }
659
- local c1 c2 c3 c4
660
- c1="$(wc -l < "$bucket_dir/bucket-1.txt" 2>/dev/null | tr -d ' ')"
661
- c2="$(wc -l < "$bucket_dir/bucket-2.txt" 2>/dev/null | tr -d ' ')"
662
- c3="$(wc -l < "$bucket_dir/bucket-3.txt" 2>/dev/null | tr -d ' ')"
663
- c4="$(wc -l < "$bucket_dir/bucket-4.txt" 2>/dev/null | tr -d ' ')"
664
- {
665
- echo "---"
666
- echo "tier_audit: combined"
667
- echo "tier_list: ${tier_list}"
668
- echo "generated_at: ${date}"
669
- echo "---"
670
- echo ""
671
- echo "# Tier-dependency audit — combined-tier (${tier_list}) — ${date}"
672
- echo ""
673
- echo "Per standards/tier-dependency-analysis.md § Combined-tier mode + ADR-052 D5. Four-bucket classification against the union of the named tiers' INTENDED sets. No 2a/2b split — in combined mode any tier tag is legit."
674
- echo ""
675
- echo "## Summary"
676
- echo ""
677
- echo "Bucket 1 (Tagged for any tier in list; ships correctly): ${c1:-0}"
678
- echo "Bucket 2 (No tag; needs per-tier assignment): ${c2:-0}"
679
- echo "Bucket 3 (True accidental leak; not in any tier's graph; needs removal): ${c3:-0}"
680
- echo "Bucket 4 (In graph but not in release; needs release fix): ${c4:-0}"
681
- echo ""
682
- local i label file
683
- for i in 1 2 3 4; do
684
- file="$bucket_dir/bucket-${i}.txt"
685
- case "$i" in
686
- 1) label="Bucket 1 — Tagged for a tier in the list (ships correctly)" ;;
687
- 2) label="Bucket 2 — No tag (needs per-tier assignment)" ;;
688
- 3) label="Bucket 3 — True accidental leak (in release but no tier's graph pulls it in)" ;;
689
- 4) label="Bucket 4 — Missing from release (in some tier's graph but release script drops it)" ;;
690
- esac
691
- echo "## ${label}"
692
- echo ""
693
- if [ -s "$file" ]; then
694
- sort "$file" | sed 's/^/- /'
695
- else
696
- echo "_(empty)_"
697
- fi
698
- echo ""
699
- done
700
- echo "## Recommended fix order"
701
- echo ""
702
- echo "1. Fix Bucket 4 first (release script gap; may hide legit ships across tiers)"
703
- echo "2. Fix Bucket 3 (remove true accidental leaks + extend .gitignore) — this is the class combined mode was built to identify safely"
704
- echo "3. Assign Bucket 2 files to a tier (operator per-file review)"
705
- } > "$outfile"
706
- return 0
707
- }
708
-
709
- # ===========================================================================
710
- # TIER GRAPH-CLOSURE EXTENSION (goal 2026-08-21a)
711
- # ===========================================================================
712
- # Extends the outbound BFS + tag reader with:
713
- # - build_reverse_index — path → callers map (for inbound walk)
714
- # - walk_graph_bfs_inbound — reverse BFS using the reverse index
715
- # - compute_kind_coverage — per REFERENCE_KIND edge count
716
- # - _extract_schema_ref — new REFERENCE_KIND for JSON schema $ref
717
- # - read_intended_source_1_extended — extends tag reader to .json/.jsonc/.yml/.py
718
- #
719
- # Anchor luminaries:
720
- # @luminary hunt-thomas — DRY (graph as single source of truth for tier);
721
- # orthogonality (each new function independent);
722
- # tracer bullets (thin end-to-end first)
723
- # @luminary alistair-cockburn — walking skeleton; hexagonal (extractors as
724
- # adapters plugging into central BFS core)
725
- # ---------------------------------------------------------------------------
726
-
727
- # ---------------------------------------------------------------------------
728
- # _is_substrate_relevant_path — filter for closure computation (G3 cure)
729
- # Interface: IPathFilter (Larman); pure predicate
730
- # Inputs: $1 = path
731
- # Outputs: exit 0 (pass) if substrate-relevant; exit 1 (skip) otherwise
732
- #
733
- # Rejects paths that pollute the mismatch report per goal 2026-08-21b Step 2:
734
- # - absolute paths + parent-relative (sibling repos via symlinks)
735
- # - .git internals
736
- # - runner's own output (state/tier-graph-closure/*)
737
- # - transient session-state markers (intent-drift-session, turn-*, session-timing)
738
- # - SDLC trace logs
739
- # ---------------------------------------------------------------------------
740
- _is_substrate_relevant_path() {
741
- local path="$1"
742
- # Normalize leading ./ prefix so patterns match consistently.
743
- # Closure files mix "./foo" (from find) with "foo" (from git grep).
744
- # Per #1385 walker cure — extends G3 filter beyond initial pattern set.
745
- path="${path#./}"
746
- case "$path" in
747
- /*|../*|.git/*|*/.git/*) return 1 ;;
748
- # Runtime state — walker outputs + gate markers + session state + analysis.
749
- # None ship to adopters regardless of tier tag. Per #1385.
750
- # Recursive state/* pattern covers all subtrees + root-level state files.
751
- # lite-manifest.json + standard-manifest.json + ultra-manifest.json live at
752
- # repo root, NOT state/, so this pattern is safe for shipping artifacts.
753
- state/*) return 1 ;;
754
- docs/sdlc-traces/*) return 1 ;;
755
- docs/preview-state/*) return 1 ;;
756
- # Per #1385 phase 2 — mirror tier-tag-required-at-write allowlist for
757
- # test fixtures (never ship; exempt from tags) + prototype galleries (dev
758
- # artifacts) + chronicles (gitignored session logs) + sibling-repo mirrors.
759
- .claude/hooks/tests/fixtures/*) return 1 ;;
760
- .claude/skills/*/tests/fixtures/*) return 1 ;;
761
- lib/tests/fixtures/*) return 1 ;;
762
- scripts/tests/fixtures/*) return 1 ;;
763
- docs/prototypes/*) return 1 ;;
764
- chronicle/*) return 1 ;;
765
- docs/chronicle/*) return 1 ;;
766
- docs/confirmed-archive/*) return 1 ;;
767
- bassclef/*) return 1 ;;
768
- esac
769
- return 0
770
- }
771
-
772
- # ---------------------------------------------------------------------------
773
- # build_reverse_index — for each seed path, find files that reference it
774
- # Interface: IReverseIndexBuilder (Larman)
775
- # Inputs: $1 = seeds file (one path per line), $2 = out file
776
- # Outputs: writes "<seed_path>\t<caller_path>" per finding
777
- # Exit: 0
778
- # Tier-blind — caller passes any tier's seed set
779
- # G3 cure (goal 2026-08-21b Step 2): callers filtered via
780
- # _is_substrate_relevant_path to keep only substrate-relevant paths.
781
- # ---------------------------------------------------------------------------
782
- build_reverse_index() {
783
- local seeds_file="$1"
784
- local out_file="$2"
785
- : > "$out_file"
786
- [ ! -s "$seeds_file" ] && return 0
787
- while IFS= read -r seed; do
788
- [ -z "$seed" ] && continue
789
- # Escape seed for grep. Use fixed-string search since paths are literal.
790
- # git grep first (respects .gitignore); fallback to plain grep if not in a repo.
791
- local matches
792
- matches="$(git grep -lF "$seed" 2>/dev/null || grep -rlF "$seed" . 2>/dev/null)"
793
- while IFS= read -r caller; do
794
- [ -z "$caller" ] && continue
795
- # Skip self-reference (a file referencing its own path is not a caller)
796
- [ "$caller" = "$seed" ] && continue
797
- # G3 cure: filter noise paths
798
- _is_substrate_relevant_path "$caller" || continue
799
- # Emit seed\tcaller
800
- printf "%s\t%s\n" "$seed" "$caller" >> "$out_file"
801
- done <<< "$matches"
802
- done < "$seeds_file"
803
- return 0
804
- }
805
-
806
- # ---------------------------------------------------------------------------
807
- # walk_graph_bfs_inbound — reverse BFS from seed set using a reverse index
808
- # Interface: IInboundGraphWalker (Larman)
809
- # Inputs: $1 = seeds file, $2 = reverse index file (from build_reverse_index)
810
- # Outputs: stdout: reachable-inbound paths, sorted-unique
811
- # Exit: 0
812
- # Cycle protection: visited set prevents re-processing (same as forward walk)
813
- # ---------------------------------------------------------------------------
814
- walk_graph_bfs_inbound() {
815
- local seeds_file="$1"
816
- local rev_index="$2"
817
- local visited_file="" queue_file=""
818
- visited_file="$(mktemp)"
819
- queue_file="$(mktemp)"
820
- # shellcheck disable=SC2064
821
- trap "rm -f '$visited_file' '$queue_file'" RETURN
822
-
823
- if [ ! -s "$seeds_file" ]; then
824
- : > "$visited_file"
825
- sort -u "$visited_file"
826
- return 0
827
- fi
828
- cp "$seeds_file" "$queue_file"
829
-
830
- local max_iter=1000 iter=0
831
- while [ -s "$queue_file" ] && [ "$iter" -lt "$max_iter" ]; do
832
- iter=$((iter + 1))
833
- local next_file; next_file="$(mktemp)"
834
- while IFS= read -r path; do
835
- [ -z "$path" ] && continue
836
- if grep -Fxq "$path" "$visited_file" 2>/dev/null; then continue; fi
837
- echo "$path" >> "$visited_file"
838
- # Look up callers of $path in the reverse index (path is column 1; caller is column 2)
839
- awk -F'\t' -v p="$path" '$1 == p { print $2 }' "$rev_index" | while IFS= read -r caller; do
840
- [ -n "$caller" ] && echo "$caller" >> "$next_file"
841
- done
842
- done < "$queue_file"
843
- mv "$next_file" "$queue_file"
844
- sort -u -o "$queue_file" "$queue_file"
845
- done
846
-
847
- sort -u "$visited_file"
848
- return 0
849
- }
850
-
851
- # ---------------------------------------------------------------------------
852
- # compute_kind_coverage — per REFERENCE_KIND edge count from a walk output
853
- # Interface: ICoverageCounter (Larman)
854
- # Inputs: $1 = walk output file with rows shaped "<kind>\t<from>\t<to>",
855
- # $2 = tier (used for report labeling)
856
- # Outputs: stdout: "<kind>\t<count>" per REFERENCE_KIND (0 for kinds not seen)
857
- # Exit: 0
858
- # ---------------------------------------------------------------------------
859
- compute_kind_coverage() {
860
- local walk_file="$1"
861
- local tier="$2"
862
- # Derive known kinds from REFERENCE_KINDS at run time so new
863
- # extractor rows automatically appear in coverage. Pre-fix this
864
- # list was hardcoded and drifted from REFERENCE_KINDS — hook_ref
865
- # existed as a walked kind but was missing from coverage output
866
- # (bug 1 per docs/audits/2026-09-10-tier-graph-walk-miss.md).
867
- # schema_ref is not in REFERENCE_KINDS but has its own extractor
868
- # per _extract_schema_ref; keep it as a static append so schema
869
- # coverage still emits.
870
- local known_kinds=()
871
- local entry name
872
- for entry in "${REFERENCE_KINDS[@]}"; do
873
- name="${entry%%|*}"
874
- known_kinds+=("$name")
875
- done
876
- known_kinds+=("schema_ref")
877
- local kind count
878
- for kind in "${known_kinds[@]}"; do
879
- count=0
880
- if [ -s "$walk_file" ]; then
881
- count="$(awk -F'\t' -v k="$kind" '$1 == k { c++ } END { print c+0 }' "$walk_file")"
882
- fi
883
- printf "%s\t%s\n" "$kind" "$count"
884
- done
885
- return 0
886
- }
887
-
888
- # ---------------------------------------------------------------------------
889
- # _extract_schema_ref — new REFERENCE_KIND for JSON schema $ref
890
- # Interface: IReferenceExtractor (Larman); Strategy per D3
891
- # Inputs: $1 = file path (typically .json schema)
892
- # Outputs: stdout: resolved schema paths (one per line)
893
- # Exit: 0 (missing file returns empty, no error)
894
- # Handles: absolute refs like "path/to/schema.json"
895
- # Skips: internal refs like "#/definitions/X" (JSON Pointer, not file ref)
896
- # ---------------------------------------------------------------------------
897
- _extract_schema_ref() {
898
- local f="$1"
899
- [ -f "$f" ] || return 0
900
- # Grep $ref values. Skip refs starting with # (internal JSON Pointer).
901
- grep -oE '"\$ref"[[:space:]]*:[[:space:]]*"[^"#][^"]*"' "$f" 2>/dev/null | \
902
- sed -E 's/.*"\$ref"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/' | \
903
- sort -u
904
- }
905
-
906
- # ---------------------------------------------------------------------------
907
- # read_intended_source_1_extended — extends tag reader to non-md/sh formats
908
- # Interface: IIntendedSourceReader (Larman); extension of read_intended_source_1
909
- # Inputs: $1 = tier
910
- # Outputs: stdout: one path per line, sorted-unique
911
- # stderr: "Source 1 (extended): <N> files"
912
- # Exit: 0
913
- # Covers ALL these tag shapes:
914
- # .md → YAML frontmatter tier: <tier> (from original)
915
- # .sh → # tier: <tier> (from original)
916
- # .json → top-level "tier": "<tier>" (new)
917
- # .json → "$comment": "tier: <tier>" (new)
918
- # .jsonc → // tier: <tier> (new)
919
- # .yml/yaml → # tier: <tier> (new)
920
- # .py → # tier: <tier> (new)
921
- # ---------------------------------------------------------------------------
922
- read_intended_source_1_extended() {
923
- local tier="$1"
924
- local out
925
- out="$({
926
- # Existing .md + .sh (mirror of read_intended_source_1)
927
- git grep -l "^tier: ${tier}$" -- '*.md' 2>/dev/null || true
928
- git grep -l "^# tier: ${tier}$" -- '*.sh' 2>/dev/null || true
929
- # .json top-level "tier": "<tier>" — allow leading { or whitespace before "tier"
930
- git grep -lE "\"tier\":[[:space:]]*\"${tier}\"" -- '*.json' 2>/dev/null || true
931
- # .json "$comment": "tier: <tier>"
932
- git grep -lE "\"\\\$comment\":[[:space:]]*\"tier:[[:space:]]*${tier}\"" -- '*.json' 2>/dev/null || true
933
- # .jsonc // tier: <tier>
934
- git grep -l "^// tier: ${tier}$" -- '*.jsonc' 2>/dev/null || true
935
- # .yml/.yaml # tier: <tier> (comment or frontmatter shape)
936
- git grep -l "^# tier: ${tier}$" -- '*.yml' '*.yaml' 2>/dev/null || true
937
- git grep -l "^tier: ${tier}$" -- '*.yml' '*.yaml' 2>/dev/null || true
938
- # .py # tier: <tier>
939
- git grep -l "^# tier: ${tier}$" -- '*.py' 2>/dev/null || true
940
- } | sort -u)"
941
- local count
942
- count="$(echo "$out" | grep -c . 2>/dev/null || echo 0)"
943
- echo "Source 1 (extended): $count files" >&2
944
- if [ -n "$out" ]; then echo "$out"; fi
945
- return 0
946
- }
947
-
948
- # ---------------------------------------------------------------------------
949
- # G5 extension (goal 2026-09-09b): per-edge tier alignment check
950
- # Ships in PR-2 alongside Bug 1 fix for coverage reporter. Closes the
951
- # gap Shape e was designed to catch — SKILL body references to hooks
952
- # tagged at higher tier than the SKILL itself (broken adopter promise
953
- # per @luminary linus-torvalds and ADR-043 D1 concentric inclusion).
954
- # ---------------------------------------------------------------------------
955
-
956
- # _tier_rank_full — full 5-tier rank map per ADR-043 D1
957
- # lite < standard < standard-pro < ultra < upstream
958
- # Rank 0 for unknown or missing tier (treated as neutral).
959
- _tier_rank_full() {
960
- case "$1" in
961
- lite) echo 1 ;;
962
- standard) echo 2 ;;
963
- standard-pro) echo 3 ;;
964
- ultra) echo 4 ;;
965
- upstream) echo 5 ;;
966
- *) echo 0 ;;
967
- esac
968
- }
969
-
970
- # _read_file_tier — extract tier tag from a file
971
- # Mirrors scripts/release-to-bassclef.sh:read_tier per standards/tier-tag-schema.md
972
- # Returns tier name on stdout; empty when no tag present.
973
- _read_file_tier() {
974
- local f="$1"
975
- [ -f "$f" ] || return 0
976
- case "$f" in
977
- *.md)
978
- awk '
979
- BEGIN{infm=0}
980
- NR==1 && /^---/ {infm=1; next}
981
- infm && /^---/ {exit}
982
- infm && /^tier:[[:space:]]/ {sub(/^tier:[[:space:]]*/,""); gsub(/[[:space:]]/,""); print; exit}
983
- ' "$f"
984
- ;;
985
- *.sh|*.py)
986
- head -5 "$f" | awk '/^# tier:[[:space:]]/ {sub(/^# tier:[[:space:]]*/,""); gsub(/[[:space:]]/,""); print; exit}'
987
- ;;
988
- *.yml|*.yaml)
989
- head -5 "$f" | awk '
990
- /^---/ {next}
991
- /^# tier:[[:space:]]/ {sub(/^# tier:[[:space:]]*/,""); gsub(/[[:space:]]/,""); print; exit}
992
- /^tier:[[:space:]]/ {sub(/^tier:[[:space:]]*/,""); gsub(/[[:space:]]/,""); print; exit}
993
- '
994
- ;;
995
- *.jsonc)
996
- head -5 "$f" | awk '/^\/\/ tier:[[:space:]]/ {sub(/^\/\/ tier:[[:space:]]*/,""); gsub(/[[:space:]]/,""); print; exit}'
997
- ;;
998
- *.json)
999
- head -5 "$f" | awk '
1000
- /"tier":[[:space:]]*"[^"]+"/ {
1001
- match($0, /"tier":[[:space:]]*"[^"]+"/); s=substr($0, RSTART, RLENGTH);
1002
- gsub(/"tier":[[:space:]]*"/, "", s); gsub(/"$/, "", s);
1003
- print s; exit
1004
- }
1005
- /"_tier":[[:space:]]*"[^"]+"/ {
1006
- match($0, /"_tier":[[:space:]]*"[^"]+"/); s=substr($0, RSTART, RLENGTH);
1007
- gsub(/"_tier":[[:space:]]*"/, "", s); gsub(/"$/, "", s);
1008
- print s; exit
1009
- }
1010
- '
1011
- ;;
1012
- *) : ;;
1013
- esac
1014
- }
1015
-
1016
- # check_tier_alignment_on_edges — per-edge tier alignment check
1017
- # Interface: ITierAlignmentChecker (per Larman GRASP naming)
1018
- # Inputs: $1 = walk-with-kinds.tsv path (rows: <kind>\t<from>\t<to>)
1019
- # $2 = target tier (e.g. "lite")
1020
- # $3 = optional out-file for violation report (default: stdout)
1021
- # Outputs: writes violations as "<kind>\t<from>\t<from-tier>\t<to>\t<to-tier>"
1022
- # Exit: 0 always. Count of violations on final stderr line.
1023
- # Rule: ADR-043 D1 concentric inclusion — source tier T guarantees
1024
- # target tier <= T. When source has target tier AND target
1025
- # file has HIGHER tier rank, source's tier promise breaks.
1026
- check_tier_alignment_on_edges() {
1027
- local walk_file="$1"
1028
- local target_tier="$2"
1029
- local out_file="${3:-/dev/stdout}"
1030
- [ -f "$walk_file" ] || { echo "check_tier_alignment_on_edges: walk file missing at $walk_file" >&2; return 1; }
1031
- local target_rank
1032
- target_rank="$(_tier_rank_full "$target_tier")"
1033
- [ "$target_rank" = "0" ] && { echo "check_tier_alignment_on_edges: unknown target tier '$target_tier'" >&2; return 1; }
1034
- # Optimization: cache tiers per unique file. Then use awk with an
1035
- # associative array for O(1) lookup during edge walk. Prior shape
1036
- # ran awk per edge lookup = O(edges * cache) ~ 10M ops on lite tier.
1037
- # Initialize local var so `set -u` callers do not trip on the trap
1038
- # when mktemp fails or the local declaration races against the trap.
1039
- local tier_cache=""
1040
- tier_cache="$(mktemp)" || tier_cache="/tmp/tier_cache_fallback_$$"
1041
- # shellcheck disable=SC2064
1042
- trap "rm -f '${tier_cache}'" RETURN
1043
- # First pass: collect unique files then read tier per file (bash loop).
1044
- local file tier
1045
- while IFS= read -r file; do
1046
- [ -n "$file" ] || continue
1047
- tier="$(_read_file_tier "$file" 2>/dev/null || true)"
1048
- printf "%s\t%s\n" "$file" "${tier:-none}"
1049
- done < <(awk -F'\t' '{print $2; print $3}' "$walk_file" | sort -u) > "$tier_cache"
1050
- # Second pass: awk-side associative array; single sweep of walk_file.
1051
- local violations
1052
- violations="$(awk -F'\t' \
1053
- -v target="$target_tier" \
1054
- -v target_rank="$target_rank" \
1055
- -v out="$out_file" \
1056
- -v cache="$tier_cache" '
1057
- BEGIN {
1058
- # Preload rank map
1059
- rank["lite"] = 1; rank["standard"] = 2; rank["standard-pro"] = 3;
1060
- rank["ultra"] = 4; rank["upstream"] = 5;
1061
- # Preload tier cache
1062
- while ((getline line < cache) > 0) {
1063
- n = split(line, parts, "\t")
1064
- if (n >= 2) tier_of[parts[1]] = parts[2]
1065
- }
1066
- close(cache)
1067
- v = 0
1068
- # Truncate out file
1069
- printf "" > out
1070
- }
1071
- {
1072
- kind = $1; from = $2; to = $3
1073
- if (kind == "" || from == "" || to == "") next
1074
- ft = tier_of[from]
1075
- tt = tier_of[to]
1076
- if (ft != target) next
1077
- tr = rank[tt] + 0
1078
- if (tr == 0) next
1079
- if (tr > target_rank) {
1080
- print kind "\t" from "\t" ft "\t" to "\t" tt >> out
1081
- v++
1082
- }
1083
- }
1084
- END { print v }
1085
- ' "$walk_file")"
1086
- echo "check_tier_alignment_on_edges: $violations violations for target_tier=$target_tier" >&2
1087
- return 0
1088
- }