@thebassclef/lite 0.0.1 → 0.1.2
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/LICENSE +202 -0
- package/README.md +85 -4
- package/dist/cli.cjs +1612 -0
- package/dist/cli.js +1589 -0
- package/dist/index.cjs +4 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -0
- package/package.json +51 -3
- package/substrate/.bassclef/lite-manifest.json +2701 -0
- package/substrate/.claude/agents/architect.md +70 -0
- package/substrate/.claude/agents/builder.md +114 -0
- package/substrate/.claude/agents/designer.md +156 -0
- package/substrate/.claude/agents/reviewer.md +88 -0
- package/substrate/.claude/hooks/artifact-ingestion-gate.sh +357 -0
- package/substrate/.claude/hooks/assert-verify-steering.sh +77 -0
- package/substrate/.claude/hooks/bassclef-source-config-validate.sh +215 -0
- package/substrate/.claude/hooks/bassclef-sync.sh +634 -0
- package/substrate/.claude/hooks/compound-noun-scrub.sh +292 -0
- package/substrate/.claude/hooks/kiss-expansion-inject.sh +69 -0
- package/substrate/.claude/hooks/longrun-prep-compounding-axis-check.sh +492 -0
- package/substrate/.claude/hooks/longrun-prep-compounding-sequence-check.sh +492 -0
- package/substrate/.claude/hooks/plain-english-steering.sh +156 -0
- package/substrate/.claude/hooks/post-skill-friction-check.sh +177 -0
- package/substrate/.claude/hooks/post-skill-telemetry.sh +62 -0
- package/substrate/.claude/hooks/pre-build-gate.sh +511 -0
- package/substrate/.claude/hooks/pre-commit-gate.sh +451 -0
- package/substrate/.claude/hooks/session-end.sh +433 -0
- package/substrate/.claude/hooks/session-reflection.sh +303 -0
- package/substrate/.claude/hooks/skill-body-grade-gate.sh +219 -0
- package/substrate/.claude/hooks/skill-body-intent-drift.sh +107 -0
- package/substrate/.claude/hooks/skill-step-list-check.sh +171 -0
- package/substrate/.claude/hooks/state-validate.sh +271 -0
- package/substrate/.claude/hooks/substrate-clarity-gate.sh +1110 -0
- package/substrate/.claude/hooks/temperance-gate.sh +147 -0
- package/substrate/.claude/hooks/testing-tier-enforce.sh +233 -0
- package/substrate/.claude/hooks/turn-prose-grade-measure.sh +219 -0
- package/substrate/.claude/hooks/turn-prose-kiss-check.sh +463 -0
- package/substrate/.claude/hooks/vocabulary-migration-check.sh +171 -0
- package/substrate/.claude/hooks/whereami-utc-gate.sh +142 -0
- package/substrate/.claude/luminaries/alan-cooper.md +170 -0
- package/substrate/.claude/luminaries/alistair-cockburn.md +140 -0
- package/substrate/.claude/luminaries/amazon-pr-faq.md +34 -0
- package/substrate/.claude/luminaries/ash-maurya.md +121 -0
- package/substrate/.claude/luminaries/bill-buxton.md +210 -0
- package/substrate/.claude/luminaries/charles-sanders-peirce.md +150 -0
- package/substrate/.claude/luminaries/david-ogilvy.md +192 -0
- package/substrate/.claude/luminaries/don-norman.md +173 -0
- package/substrate/.claude/luminaries/edward-tufte.md +179 -0
- package/substrate/.claude/luminaries/eric-evans.md +160 -0
- package/substrate/.claude/luminaries/frederick-brooks.md +50 -0
- package/substrate/.claude/luminaries/gang-of-four.md +157 -0
- package/substrate/.claude/luminaries/glenford-myers.md +230 -0
- package/substrate/.claude/luminaries/hunt-thomas.md +115 -0
- package/substrate/.claude/luminaries/hyrum-wright.md +94 -0
- package/substrate/.claude/luminaries/jason-fried-dhh.md +46 -0
- package/substrate/.claude/luminaries/jesse-james-garrett.md +154 -0
- package/substrate/.claude/luminaries/john-ousterhout.md +94 -0
- package/substrate/.claude/luminaries/karl-popper.md +132 -0
- package/substrate/.claude/luminaries/kent-beck.md +168 -0
- package/substrate/.claude/luminaries/linus-torvalds.md +218 -0
- package/substrate/.claude/luminaries/martin-fowler.md +164 -0
- package/substrate/.claude/luminaries/michael-feathers.md +125 -0
- package/substrate/.claude/luminaries/michael-nygard.md +118 -0
- package/substrate/.claude/luminaries/robert-c-martin.md +164 -0
- package/substrate/.claude/luminaries/saltzer-schroeder.md +194 -0
- package/substrate/.claude/luminaries/sophia-prater.md +193 -0
- package/substrate/.claude/luminaries/stephen-toulmin.md +131 -0
- package/substrate/.claude/luminaries/tony-hoare.md +170 -0
- package/substrate/.claude/luminaries/vaughn-vernon.md +50 -0
- package/substrate/.claude/luminaries/w-edwards-deming.md +158 -0
- package/substrate/.claude/rules/accessor-library-discipline.md +138 -0
- package/substrate/.claude/rules/adr-discipline.md +120 -0
- package/substrate/.claude/rules/api-conventions.md +125 -0
- package/substrate/.claude/rules/artifact-ingestion.md +179 -0
- package/substrate/.claude/rules/assert-only-after-verify.md +137 -0
- package/substrate/.claude/rules/blocked-items.md +146 -0
- package/substrate/.claude/rules/bootstrap-pair-discipline.md +141 -0
- package/substrate/.claude/rules/branching.md +28 -0
- package/substrate/.claude/rules/cold-adopter-harness-discipline.md +129 -0
- package/substrate/.claude/rules/commit-conventions.md +22 -0
- package/substrate/.claude/rules/compounding-axis-fresh-analysis.md +188 -0
- package/substrate/.claude/rules/compounding-sequence-fresh-analysis.md +188 -0
- package/substrate/.claude/rules/context-engineering.md +202 -0
- package/substrate/.claude/rules/context-management.md +85 -0
- package/substrate/.claude/rules/defensive-bash.md +68 -0
- package/substrate/.claude/rules/deferred-actions.md +233 -0
- package/substrate/.claude/rules/destructive-operations.md +69 -0
- package/substrate/.claude/rules/diagnosis.md +38 -0
- package/substrate/.claude/rules/github-issue-flash-tweet.md +156 -0
- package/substrate/.claude/rules/guardrails.md +73 -0
- package/substrate/.claude/rules/hook-wire-on-author.md +103 -0
- package/substrate/.claude/rules/identifier-leak-prevention.md +104 -0
- package/substrate/.claude/rules/iteration-bet-brief-completeness.md +54 -0
- package/substrate/.claude/rules/lite-manifest-schema-change-discipline.md +98 -0
- package/substrate/.claude/rules/longrun-prep-plan-doc-compression.md +89 -0
- package/substrate/.claude/rules/loop-discipline.md +81 -0
- package/substrate/.claude/rules/manual-prod-approval.md +100 -0
- package/substrate/.claude/rules/marker-enrichment-discipline.md +99 -0
- package/substrate/.claude/rules/mobile-ephemeral-session.md +109 -0
- package/substrate/.claude/rules/new-dependency-check.md +51 -0
- package/substrate/.claude/rules/oo-ad-entry-point.md +117 -0
- package/substrate/.claude/rules/operator-facing-prose.md +196 -0
- package/substrate/.claude/rules/option-label-discipline.md +108 -0
- package/substrate/.claude/rules/pattern-annotation.md +100 -0
- package/substrate/.claude/rules/plain-english-discipline.md +156 -0
- package/substrate/.claude/rules/plan-enumeration-needs-value-props.md +211 -0
- package/substrate/.claude/rules/pr-body-shape.md +317 -0
- package/substrate/.claude/rules/pr-strategy.md +167 -0
- package/substrate/.claude/rules/pr-title-shape.md +161 -0
- package/substrate/.claude/rules/prototype-workflow.md +65 -0
- package/substrate/.claude/rules/reserved-skill-names.md +123 -0
- package/substrate/.claude/rules/schema-management.md +49 -0
- package/substrate/.claude/rules/sdlc-gates.md +149 -0
- package/substrate/.claude/rules/security.md +37 -0
- package/substrate/.claude/rules/session-artifacts.md +236 -0
- package/substrate/.claude/rules/skill-composition-declarations.md +124 -0
- package/substrate/.claude/rules/skill-description-clarity.md +247 -0
- package/substrate/.claude/rules/skill-procedure-step-list.md +137 -0
- package/substrate/.claude/rules/state-schema-validation.md +162 -0
- package/substrate/.claude/rules/stuck-signal-diagnostic.md +140 -0
- package/substrate/.claude/rules/substrate-config-schema.md +98 -0
- package/substrate/.claude/rules/test-list-discipline.md +175 -0
- package/substrate/.claude/rules/test-sufficiency.md +210 -0
- package/substrate/.claude/rules/testing-tier-config.md +145 -0
- package/substrate/.claude/rules/testing.md +38 -0
- package/substrate/.claude/rules/turn-estimate-grounding.md +134 -0
- package/substrate/.claude/rules/visual-hierarchy.md +437 -0
- package/substrate/.claude/rules/we-dont-break-adopters.md +126 -0
- package/substrate/.claude/rules/whereami-load-bearing.md +202 -0
- package/substrate/.claude/rules/writing-craft-discipline.md +92 -0
- package/substrate/.claude/rules/wu-sequencing-compounds.md +145 -0
- package/substrate/.claude/skills/build/SKILL.md +640 -0
- package/substrate/.claude/skills/chronicle/SKILL.md +55 -0
- package/substrate/.claude/skills/clean-artifacts/SKILL.md +249 -0
- package/substrate/.claude/skills/decompose/SKILL.md +280 -0
- package/substrate/.claude/skills/diagnose/SKILL.md +297 -0
- package/substrate/.claude/skills/feynman/SKILL.md +90 -0
- package/substrate/.claude/skills/howdoi/SKILL.md +105 -0
- package/substrate/.claude/skills/ia-model/SKILL.md +108 -0
- package/substrate/.claude/skills/interaction-design/SKILL.md +112 -0
- package/substrate/.claude/skills/interpret-input/SKILL.md +180 -0
- package/substrate/.claude/skills/journal/SKILL.md +209 -0
- package/substrate/.claude/skills/kiss/SKILL.md +449 -0
- package/substrate/.claude/skills/launch/SKILL.md +915 -0
- package/substrate/.claude/skills/lean-canvas/SKILL.md +332 -0
- package/substrate/.claude/skills/longrun/SKILL.md +463 -0
- package/substrate/.claude/skills/luminary/SKILL.md +481 -0
- package/substrate/.claude/skills/ogilvy-writing-audit/SKILL.md +177 -0
- package/substrate/.claude/skills/onboard-repo/SKILL.md +1624 -0
- package/substrate/.claude/skills/pattern-review/SKILL.md +99 -0
- package/substrate/.claude/skills/personas/SKILL.md +207 -0
- package/substrate/.claude/skills/promote/SKILL.md +283 -0
- package/substrate/.claude/skills/requirement/SKILL.md +98 -0
- package/substrate/.claude/skills/retro/SKILL.md +117 -0
- package/substrate/.claude/skills/riff/SKILL.md +114 -0
- package/substrate/.claude/skills/roadmap-reconcile/SKILL.md +163 -0
- package/substrate/.claude/skills/session-end/SKILL.md +309 -0
- package/substrate/.claude/skills/session-log/SKILL.md +299 -0
- package/substrate/.claude/skills/skills/SKILL.md +228 -0
- package/substrate/.claude/skills/spec/SKILL.md +105 -0
- package/substrate/.claude/skills/sprint/SKILL.md +392 -0
- package/substrate/.claude/skills/stage/SKILL.md +384 -0
- package/substrate/.claude/skills/state-a-problem/SKILL.md +185 -0
- package/substrate/.claude/skills/temperance/SKILL.md +108 -0
- package/substrate/.claude/skills/use-case/SKILL.md +417 -0
- package/substrate/.claude/skills/user-stories/SKILL.md +268 -0
- package/substrate/.claude/skills/value-prop/SKILL.md +251 -0
- package/substrate/.claude/skills/verify/SKILL.md +160 -0
- package/substrate/.claude/skills/visual-review/SKILL.md +503 -0
- package/substrate/.claude/skills/whats-the-plan/SKILL.md +202 -0
- package/substrate/.claude/skills/whereami/SKILL.md +307 -0
- package/substrate/AGENTS.md +79 -0
- package/substrate/CLAUDE-lite.md +85 -0
- package/substrate/CODE_OF_CONDUCT.md +28 -0
- package/substrate/CONTRIBUTING.md +177 -0
- package/substrate/README.md +173 -0
- package/substrate/SECURITY.md +19 -0
- package/substrate/architecture/decisions/ADR-029-release-pipeline.md +79 -0
- package/substrate/architecture/decisions/ADR-031-non-breaking-changes-adopter-discipline.md +139 -0
- package/substrate/architecture/decisions/ADR-032-adopter-sync-dispatcher-architecture.md +192 -0
- package/substrate/architecture/decisions/ADR-039-release-tagging-scheme.md +145 -0
- package/substrate/architecture/decisions/ADR-040-planning-skill-vocabulary-and-lite-profile.md +155 -0
- package/substrate/architecture/decisions/ADR-044-unified-skill-body-template.md +162 -0
- package/substrate/lib/clean-artifacts-sweep.sh +112 -0
- package/substrate/lib/code-comment-discipline.sh +144 -0
- package/substrate/lib/composer-preflight.sh +459 -0
- package/substrate/lib/hook-inject.sh +255 -0
- package/substrate/lib/luminary-pick.sh +96 -0
- package/substrate/lib/output-discipline.sh +143 -0
- package/substrate/lib/prose-scan-boundary.sh +171 -0
- package/substrate/lib/rewrite-check.sh +214 -0
- package/substrate/lib/state.sh +1372 -0
- package/substrate/lib/telemetry.sh +205 -0
- package/substrate/lib/tier-check.sh +187 -0
- package/substrate/lib/tier-dependency-audit.sh +1088 -0
- package/substrate/presence/install/bassclef-hook-connect.sh +178 -0
- package/substrate/presence/install/bassclef-sync.dispatcher.template.sh +841 -0
- package/substrate/presence/install/bassclef-sync.template.sh +2076 -0
- package/substrate/presence/install/schedule-auto-save.cron.sh +88 -0
- package/substrate/presence/install/schedule-auto-save.taskscheduler.md +122 -0
- package/substrate/scripts/aggregate-telemetry.sh +217 -0
- package/substrate/scripts/analyze-tier-dependencies.sh +239 -0
- package/substrate/scripts/generate-lite-manifest.sh +505 -0
- package/substrate/scripts/generate-tier-manifest.sh +28 -0
- package/substrate/scripts/intent-drift-check.sh +456 -0
- package/substrate/scripts/lite-manifest-drift-check.sh +146 -0
- package/substrate/scripts/render-lite-manifest-doc.sh +150 -0
- package/substrate/standards/adr-template.md +86 -0
- package/substrate/standards/api-conventions/nextjs.md +84 -0
- package/substrate/standards/artifact-composition.md +209 -0
- package/substrate/standards/bash-hook-safety.md +246 -0
- package/substrate/standards/bassclef-configs-schema.md +232 -0
- package/substrate/standards/bassclef-evolution.md +143 -0
- package/substrate/standards/bassclef-internal-jargon.md +244 -0
- package/substrate/standards/bassclef-managed-sentinel.md +96 -0
- package/substrate/standards/bassclef-source-config.md +228 -0
- package/substrate/standards/branch-stacking.md +408 -0
- package/substrate/standards/code-safety-principles.md +176 -0
- package/substrate/standards/composer-prerequisites.md +155 -0
- package/substrate/standards/deferred-actions-schema.md +204 -0
- package/substrate/standards/dependency-discipline/cargo.md +39 -0
- package/substrate/standards/dependency-discipline/gem.md +43 -0
- package/substrate/standards/dependency-discipline/go-mod.md +41 -0
- package/substrate/standards/dependency-discipline/npm.md +42 -0
- package/substrate/standards/dependency-discipline/pip.md +42 -0
- package/substrate/standards/deployment-topology/ec2-tailscale.md +225 -0
- package/substrate/standards/deployment-topology.md +69 -0
- package/substrate/standards/docs-sync-allowlist.md +76 -0
- package/substrate/standards/domain-and-dns.md +145 -0
- package/substrate/standards/frontend-stack.md +67 -0
- package/substrate/standards/frontmatter-schema.md +154 -0
- package/substrate/standards/graceful-exit.md +227 -0
- package/substrate/standards/hook-idempotency.md +102 -0
- package/substrate/standards/hook-injection-discipline.md +202 -0
- package/substrate/standards/hook-install-class.md +215 -0
- package/substrate/standards/input-handler-interface.md +152 -0
- package/substrate/standards/lite-manifest-schema-changes.md +135 -0
- package/substrate/standards/luminary-matching.md +105 -0
- package/substrate/standards/luminary-problem-patterns.md +481 -0
- package/substrate/standards/migration-discipline/active-record.md +50 -0
- package/substrate/standards/migration-discipline/alembic.md +43 -0
- package/substrate/standards/migration-discipline/gorm.md +50 -0
- package/substrate/standards/migration-discipline/prisma.md +53 -0
- package/substrate/standards/migration-discipline/sqlalchemy.md +51 -0
- package/substrate/standards/mobile-ephemeral-session.md +167 -0
- package/substrate/standards/model-routing-discipline.md +160 -0
- package/substrate/standards/ogilvy-writing-rules.md +225 -0
- package/substrate/standards/opener-discipline.md +96 -0
- package/substrate/standards/operator-facing-prose-discipline.md +201 -0
- package/substrate/standards/persona-schema.md +229 -0
- package/substrate/standards/pluggable-luminaries.md +323 -0
- package/substrate/standards/pr-body-discipline.md +115 -0
- package/substrate/standards/preview-state-schema.md +189 -0
- package/substrate/standards/project-directory-layout.md +276 -0
- package/substrate/standards/release-tagging.md +137 -0
- package/substrate/standards/reserved-skill-names.md +120 -0
- package/substrate/standards/scannable-multi-option-output.md +261 -0
- package/substrate/standards/sdlc-compliance.md +286 -0
- package/substrate/standards/sdlc-gates/typescript.md +57 -0
- package/substrate/standards/secrets-lifecycle.md +210 -0
- package/substrate/standards/security-scanner-adapter.md +145 -0
- package/substrate/standards/session-board.md +256 -0
- package/substrate/standards/skill-output-discipline.md +90 -0
- package/substrate/standards/state-spine-contract.md +255 -0
- package/substrate/standards/state-spine.md +511 -0
- package/substrate/standards/steering-hints/kiss-words.md +11 -0
- package/substrate/standards/substrate-config-schema.md +267 -0
- package/substrate/standards/tech-stack-config.md +109 -0
- package/substrate/standards/tier-dependency-analysis.md +167 -0
- package/substrate/standards/tier-runtime-deps/lite.md +57 -0
- package/substrate/standards/tier-tag-schema.md +155 -0
- package/substrate/standards/two-layer-config.md +99 -0
- package/substrate/standards/use-case-format.md +292 -0
- package/substrate/standards/user-story-invest.md +268 -0
- package/substrate/standards/velocity-and-appetite.md +229 -0
- package/substrate/standards/voice-input-pattern.md +119 -0
- package/substrate/standards/whereami-schema.md +301 -0
- package/substrate/standards/worktree-management.md +211 -0
- package/substrate/standards/writing-guide.md +213 -0
- package/substrate/templates/chronicle-template.md +75 -0
- package/substrate/templates/deferred-action-template.md +45 -0
- package/substrate/templates/memory-proposal-template.md +77 -0
- package/substrate/templates/persona-template.md +200 -0
- package/substrate/templates/pr-faq.md +45 -0
- package/substrate/templates/secret-rotation-template.md +162 -0
- package/substrate/templates/spec-template.md +131 -0
- package/substrate/templates/use-case-template.md +194 -0
- package/substrate/templates/user-story-template.md +107 -0
- package/substrate/templates/whereami-template.md +101 -0
|
@@ -0,0 +1,1088 @@
|
|
|
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
|
+
}
|