azcodr 1.5.1 → 2.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.
- package/.agents/hooks.json.example +42 -42
- package/.agents/scripts/safety_guard.sh +143 -34
- package/.agents/scripts/verify_completion.sh +90 -27
- package/.agents/skills/agentic-architect/SKILL.md +125 -125
- package/.agents/skills/agentic-architect/references/agents_md_template.md +62 -62
- package/.agents/skills/agentic-architect/references/refinement_workflow.md +32 -32
- package/.agents/skills/agentic-architect/references/skill_architecture_inquiry.md +63 -63
- package/.agents/skills/agentic-architect/references/skill_template.md +56 -56
- package/.agents/skills/agentic-architect/scripts/validate_agentic_configs.sh +402 -401
- package/.agents/skills/clean-code-refactor/SKILL.md +91 -91
- package/.agents/skills/clean-code-refactor/references/clean_code_smells.md +27 -27
- package/.agents/skills/clean-code-refactor/references/design_patterns_ts.md +65 -65
- package/.agents/skills/compliance-audit/SKILL.md +120 -120
- package/.agents/skills/compliance-audit/references/owasp_top10_controls.md +16 -16
- package/.agents/skills/compliance-audit/references/soc2_iso_controls.md +28 -28
- package/.agents/skills/lets-build/SKILL.md +173 -173
- package/.agents/skills/lets-build/references/architecture_interview_matrix.md +115 -115
- package/.agents/skills/lets-build/references/hexagonal_bootstrap_scaffolds.md +160 -160
- package/.agents/skills/lets-build/references/project_readme_template.md +79 -79
- package/.agents/skills/lets-build/scripts/bootstrap_workspace.sh +419 -255
- package/.agents/skills/product-analyst/SKILL.md +154 -154
- package/.agents/skills/product-analyst/references/backlog_ordering_techniques.md +107 -107
- package/.agents/skills/product-analyst/references/gherkin_patterns.md +46 -46
- package/.agents/skills/product-analyst/references/invest_checklist.md +38 -38
- package/.agents/skills/product-analyst/references/okr_alignment_guide.md +76 -76
- package/.agents/skills/product-analyst/references/smart_tasks.md +59 -59
- package/.agents/skills/relentless-questioner/SKILL.md +128 -128
- package/.agents/skills/relentless-questioner/references/adaptive_question_trees.md +102 -102
- package/.editorconfig +19 -19
- package/.github/workflows/ci.yml +167 -56
- package/.github/workflows/publish.yml +200 -0
- package/.gitignore +40 -25
- package/AGENTS.md +103 -102
- package/LICENSE +21 -21
- package/README.md +168 -154
- package/bin/azcodr.js +14 -228
- package/docs/knowledge/ubiquitous_language.md +31 -18
- package/docs/rules/agentic_configuration.md +259 -259
- package/docs/rules/api_architecture.md +179 -179
- package/docs/rules/authentication.md +76 -76
- package/docs/rules/authorization.md +75 -75
- package/docs/rules/caching.md +69 -69
- package/docs/rules/clean_code.md +62 -62
- package/docs/rules/cloud_native.md +41 -41
- package/docs/rules/cqrs.md +203 -203
- package/docs/rules/database_design.md +125 -125
- package/docs/rules/database_operations.md +69 -69
- package/docs/rules/design_patterns.md +98 -98
- package/docs/rules/devops_ci_cd.md +76 -76
- package/docs/rules/domain_driven_design.md +122 -122
- package/docs/rules/error_handling.md +54 -52
- package/docs/rules/feature_flags.md +59 -59
- package/docs/rules/frontend_architecture.md +157 -157
- package/docs/rules/multitenancy_architecture.md +98 -98
- package/docs/rules/product_ownership.md +127 -127
- package/docs/rules/project_management.md +49 -49
- package/docs/rules/relentless_questioning.md +52 -52
- package/docs/rules/requirements_engineering.md +98 -98
- package/docs/rules/security_compliance.md +53 -53
- package/docs/rules/server_driven_ui.md +88 -88
- package/docs/rules/test_driven_development.md +185 -185
- package/docs/rules/transactional_email.md +27 -27
- package/docs/rules/type_safety.md +65 -65
- package/docs/rules/ui_ux_architecture.md +150 -150
- package/docs/rules/workflow_state_machines.md +117 -117
- package/lib/cli-parse.js +51 -0
- package/lib/cli-target.js +109 -0
- package/lib/cli.js +180 -0
- package/lib/errors.js +28 -0
- package/lib/git.js +29 -0
- package/lib/guards.js +96 -0
- package/lib/index.d.ts +199 -134
- package/lib/index.js +5 -5
- package/lib/links.js +123 -0
- package/lib/permissions.js +44 -0
- package/lib/repo.js +90 -0
- package/lib/scaffold.js +238 -399
- package/memory.md +119 -36
- package/package.json +65 -62
- package/scripts/test_coverage.js +66 -38
- package/scripts/validate/adr.js +151 -0
- package/scripts/validate/io.js +84 -0
- package/scripts/validate/links.js +167 -0
- package/scripts/validate/parity.js +124 -0
- package/scripts/validate/root.js +184 -0
- package/scripts/validate/rules.js +44 -0
- package/scripts/validate/skills.js +96 -0
- package/scripts/validate/text.js +29 -0
- package/scripts/validate-cli.js +13 -0
- package/scripts/validate.js +112 -218
- package/.github/copilot-instructions.md +0 -1
package/memory.md
CHANGED
|
@@ -1,36 +1,119 @@
|
|
|
1
|
-
# Workspace Memory, Architecture Decisions & Knowledge Hub
|
|
2
|
-
|
|
3
|
-
> **Core Purpose:** Authoritative persistent memory ledger for the workspace repository (`./`), maintaining Lightweight Architectural Decision Records (ADRs), system topologies, and living domain contracts.
|
|
4
|
-
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
## 1. Quick Navigation & Knowledge Repositories
|
|
8
|
-
|
|
9
|
-
- 📖 **[Living Ubiquitous Language Glossary](./docs/knowledge/ubiquitous_language.md)**: Authoritative, single-name domain vocabulary contract.
|
|
10
|
-
- 📜 **[Lightweight ADR Master Index](#adr-master-index)**: Summary of all architectural decisions and direct links to governing rules.
|
|
11
|
-
|
|
12
|
-
---
|
|
13
|
-
|
|
14
|
-
## 2. Consolidated Architectural Decision Records (ADRs)
|
|
15
|
-
|
|
16
|
-
### ADR Master Index
|
|
17
|
-
|
|
18
|
-
| ID | Title | Date | Status | Governing Rule / Skill |
|
|
19
|
-
|---|---|---|---|---|
|
|
20
|
-
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
1
|
+
# Workspace Memory, Architecture Decisions & Knowledge Hub
|
|
2
|
+
|
|
3
|
+
> **Core Purpose:** Authoritative persistent memory ledger for the workspace repository (`./`), maintaining Lightweight Architectural Decision Records (ADRs), system topologies, and living domain contracts.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. Quick Navigation & Knowledge Repositories
|
|
8
|
+
|
|
9
|
+
- 📖 **[Living Ubiquitous Language Glossary](./docs/knowledge/ubiquitous_language.md)**: Authoritative, single-name domain vocabulary contract.
|
|
10
|
+
- 📜 **[Lightweight ADR Master Index](#adr-master-index)**: Summary of all architectural decisions and direct links to governing rules.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## 2. Consolidated Architectural Decision Records (ADRs)
|
|
15
|
+
|
|
16
|
+
### ADR Master Index
|
|
17
|
+
|
|
18
|
+
| ID | Title | Date | Status | Governing Rule / Skill |
|
|
19
|
+
|---|---|---|---|---|
|
|
20
|
+
| [ADR-001](./memory.md#adr-001-gate-the-validator-itself) | Gate the validator itself | 2026-10-07 | ACCEPTED | [`test_driven_development.md`](./docs/rules/test_driven_development.md) |
|
|
21
|
+
| [ADR-002](./memory.md#adr-002-separate-library-from-process-entry) | Separate library from process entry | 2026-10-07 | ACCEPTED | [`clean_code.md`](./docs/rules/clean_code.md) |
|
|
22
|
+
| [ADR-003](./memory.md#adr-003-give-every-throw-site-a-stable-error-code) | Give every throw site a stable error code | 2026-10-07 | ACCEPTED | [`error_handling.md`](./docs/rules/error_handling.md) |
|
|
23
|
+
| [ADR-004](./memory.md#adr-004-guard-the-self-healing-parity-repair-against-case-insensitive-filesystems) | Guard the self-healing parity repair | 2026-10-07 | ACCEPTED | [`agentic_configuration.md`](./docs/rules/agentic_configuration.md) |
|
|
24
|
+
| [ADR-005](./memory.md#adr-005-verify-hooks-do-not-assume-them) | Verify hooks, do not assume them | 2026-10-07 | ACCEPTED | [`agentic_configuration.md`](./docs/rules/agentic_configuration.md) |
|
|
25
|
+
| [ADR-006](./memory.md#adr-006-the-validator-fails-closed-on-degraded-input) | The validator fails closed on degraded input | 2026-10-07 | ACCEPTED | [`test_driven_development.md`](./docs/rules/test_driven_development.md) |
|
|
26
|
+
| [ADR-007](./memory.md#adr-007-publish-from-ci-never-from-a-workstation) | Publish from CI, never from a workstation | 2026-10-07 | ACCEPTED | [`devops_ci_cd.md`](./docs/rules/devops_ci_cd.md) |
|
|
27
|
+
| [ADR-008](./memory.md#adr-008-jsr-publishing-blocked-pending-an-esm-port) | JSR publishing blocked pending an ESM port | 2026-10-07 | PROPOSED | [`devops_ci_cd.md`](./docs/rules/devops_ci_cd.md) |
|
|
28
|
+
| [ADR-009](./memory.md#adr-009-memorymd-is-append-only-the-scaffolder-never-rewrites-the-ledger) | memory.md is append-only; the scaffolder never rewrites it | 2026-10-07 | ACCEPTED | [`agentic_configuration.md`](./docs/rules/agentic_configuration.md) |
|
|
29
|
+
| [ADR-010](./memory.md#adr-010-the-scaffolder-refuses-protected-targets) | The scaffolder refuses protected targets | 2026-10-07 | ACCEPTED | [`security_compliance.md`](./docs/rules/security_compliance.md) |
|
|
30
|
+
| [ADR-011](./memory.md#adr-011-the-bash-scaffolder-refuses-protected-targets) | The bash scaffolder refuses protected targets | 2026-10-07 | ACCEPTED | [`security_compliance.md`](./docs/rules/security_compliance.md) |
|
|
31
|
+
| [ADR-012](./memory.md#adr-012-enforce-the-file-and-function-gates-the-repo-prescribes) | Enforce the file and function gates the repo prescribes | 2026-10-07 | ACCEPTED | [`clean_code.md`](./docs/rules/clean_code.md) |
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
### Lightweight Decision Summaries
|
|
36
|
+
|
|
37
|
+
#### ADR-001: Gate the Validator Itself
|
|
38
|
+
- **Date:** 2026-10-07 | **Status:** ACCEPTED
|
|
39
|
+
- **Context:** The 100.00% coverage badge covered only `bin/` and `lib/`. `scripts/validate.js` (259 lines of control flow) and `scripts/test_coverage.js` had **zero** tests, so the validator every downstream project depends on shipped unverified while the repo reported full coverage. The same class of hole appeared twice in sibling repos: stirling-engine's ADR-004 ("compile the core lib" ≠ "the project builds") and typed-settings, where declared-but-never-invoked `--coverage` left the same 100% threshold unenforced.
|
|
40
|
+
- **Decision:** (1) Add `scripts/` to the coverage gate so the validator cannot hide. (2) Refactor `runValidation` into a pure, injectable-reporter function so it is testable at all — it previously self-executed at `require` time. (3) Extract `evaluateParityTarget` as I/O-free decision logic, because symlink creation returns EPERM on Windows without Developer Mode and those verdicts were otherwise untestable on the platform that matters most. (4) Split the suite into a happy-path tier and a `break-*` adversarial tier named after fault classes, ported from typed-settings. (5) Add architecture tests asserting the invariants that were previously only prose: no shell-string spawning, git subcommand allowlist excludes `push`/`clone`/`reset`, zero runtime deps, `TEMPLATE_ITEMS` and `package.json` `files[]` resolve, README rule count matches reality.
|
|
41
|
+
- **Consequences:** ✅ 74 → 194 tests; `validate.js` reaches 100/100/100. ✅ The `0dbdde0` shell-injection fix is now protected by a regression test instead of being historical. ✅ Adversarial tests found three real defects, fixed below. ⚠️ `scripts/validate-cli.js` is excluded from the gate by explicit include path (a two-line process entry unreachable in-process); a test asserts every *other* script stays inside the gate so the exclusion cannot widen silently.
|
|
42
|
+
- **Enforced In:** `scripts/test_coverage.js`, `scripts/validate.js`, `tests/validate*.test.js`, `tests/architecture.test.js`, `tests/coverage-config.test.js`
|
|
43
|
+
|
|
44
|
+
#### ADR-002: Separate Library From Process Entry
|
|
45
|
+
- **Date:** 2026-10-07 | **Status:** ACCEPTED
|
|
46
|
+
- **Context:** A `require.main === module` guard in `scripts/validate.js` is unreachable under an in-process test runner, leaving one permanently-uncovered branch that blocked the 100% branch gate. `v8 ignore` did not resolve it. Meanwhile requiring the validator during tests triggered a full validation run and `process.exit`.
|
|
47
|
+
- **Decision:** The library exposes `main(argv, exit)` with an injectable exit function and no self-execution; `scripts/validate-cli.js` is the two-line process entry. Coverage uses explicit `--test-coverage-include` paths rather than `scripts/**` so the shim is excluded by name, not by wildcard accident.
|
|
48
|
+
- **Consequences:** ✅ 100% branch coverage reachable without suppressions. ✅ `require('scripts/validate.js')` is safe from any tool. ✅ Scaffolded projects get the shim automatically (it ships in `scripts/`) and their `validate` script points at it. ⚠️ One extra file in every scaffolded project.
|
|
49
|
+
- **Enforced In:** `scripts/validate-cli.js`, `scripts/validate.js`, `lib/scaffold.js`, `tests/coverage-config.test.js`
|
|
50
|
+
|
|
51
|
+
#### ADR-012: Enforce the File and Function Gates the Repo Prescribes
|
|
52
|
+
- **Date:** 2026-10-07 | **Status:** ACCEPTED
|
|
53
|
+
- **Context:** `docs/rules/clean_code.md` section 5 mandates build-failing fitness functions (ESLint `max-lines`, `max-lines-per-function`, complexity ≤ 10, dependency gates: "If an AI attempt to add code violates any fitness function, the build fails immediately"), yet `package.json` defined `lint` as `node --check` — a syntax parse with no rules — and the repo violated its own caps everywhere: 12 files over 250 lines (`validate.js` 784, `scaffold.test.js` 666, `scaffold.js` 555), `runValidation` at 378 lines with complexity 57, `runCli` at complexity 49, and the linter itself could not see any of it. Separately, the architecture test forbade even devDependencies, which made the mandated ESLint literally uninstallable without breaking a guard.
|
|
54
|
+
- **Decision:** (1) Split the invariant the zero-dep test conflated: **runtime** dependencies stay forbidden (the consumer supply-chain guarantee — nothing a user installs executes third-party code), while contributor tooling is allowed as devDependencies IFF every entry is exact-pinned and `package-lock.json` is committed, so all contributors install byte-identical tooling that never ships (the tarball is bounded by `files[]`, and npm never installs a dependency's devDependencies). (2) ESLint 10.12.0 (exact) with a flat config enforcing exactly the documented gates: `max-lines` 300, `max-lines-per-function` 30, `complexity` 10, `max-params` 3. Dependency-direction gates stay as architecture TESTS rather than ESLint plugins — tests need no plugins and therefore add no supply-chain surface. (3) Refactored to comply: `lib/` into 6 modules plus a thin orchestrator, `scripts/validate.js` (784) into 8 modules, `bin/` logic into `lib/cli-parse.js`, `lib/cli-target.js`, `lib/cli.js` with `bin/azcodr.js` as a process-entry shim, and 9 test files split into focused files (54 of the 87 violations were test callbacks over 30 lines). (4) `ensureSymlink`/`ensureSymlinkOrPointer` took 4 positional params, so both now take a single options object — a breaking programmatic-API change, hence the major bump to 2.0.0.
|
|
55
|
+
- **Consequences:** ✅ 87 lint errors → 0, and `npm run lint` now fails the build on any new violation instead of parsing syntax. ✅ Tests went 397 → 412 with zero lost (reconciled per file against HEAD; the +15 is one new stat-failure test, one pre-existing duplicate preserved verbatim, and loop-generated cases from splits). ✅ Coverage gates hold across all 17 source files. ⚠️ Contributors now run `npm install` (dev-only); consumers are unaffected. ⚠️ 2.0.0 is semver-major solely for the two options-object signatures; the CLI surface is unchanged.
|
|
56
|
+
- **Enforced In:** `eslint.config.js`, `package-lock.json`, `lib/`, `scripts/validate/`, `lib/cli-*.js`, `tests/architecture.test.js`, `tests/coverage-config.test.js`, `package.json` (`lint`, `version`)
|
|
57
|
+
|
|
58
|
+
#### ADR-011: The Bash Scaffolder Refuses Protected Targets
|
|
59
|
+
- **Date:** 2026-10-07 | **Status:** ACCEPTED
|
|
60
|
+
- **Context:** ADR-010 closed the protected-target hole for the Node scaffolder (`lib/scaffold.js`), but the same hole existed in `.agents/skills/lets-build/scripts/bootstrap_workspace.sh`, which runs with no guard at all: invoked with `/` or `~` as the workspace root it would create `src/`, `tests/`, `deploy/`, `specs/` trees there. Its blast radius is smaller than the Node scaffolder's (every file write is create-only — `tokens.json` and `smoke_test.sh` are written only if absent, and `memory.md` is append-only since ADR-009 — except the explicit `AZCODR_RESET_MEMORY=1` reset), so this is pollution rather than destruction. Pollution of `/` or `~` is still unacceptable for a script agents run autonomously.
|
|
61
|
+
- **Decision:** A protected-target check runs before anything is created — including the `.azcodr` profile write. It refuses `/`, Git-Bash and native drive roots (`/c`, `C:\`), the home directory, and the home directory's parent, with exit 2 and a message naming the remedy. `AZCODR_ALLOW_PROTECTED=1` bypasses it for embedders, mirroring the Node side's `allowProtected` option (which the CLI never passes, just as the skill never sets this variable unless instructed). Existing targets are resolved with `pwd -P` so symlink aliases are caught; comparisons are lowercased via `tr` rather than `${var,,}` because macOS ships bash 3.2.
|
|
62
|
+
- **Consequences:** ✅ `tests/bootstrap-scripts.test.js` proves it: the three refusal tests fail against the unguarded script and pass with the guard. ✅ The bypass and the normal-subdirectory case are tested, so the guard cannot over-block. ✅ Both scaffolders now enforce the same invariant, stated in one place per tool. ⚠️ Drive-root detection covers `/x` and `X:\` spellings; a UNC path (`\\server\share`) is not specially handled and falls through to the normal flow.
|
|
63
|
+
- **Enforced In:** `.agents/skills/lets-build/scripts/bootstrap_workspace.sh`, `tests/bootstrap-scripts.test.js`
|
|
64
|
+
|
|
65
|
+
#### ADR-010: The Scaffolder Refuses Protected Targets
|
|
66
|
+
- **Date:** 2026-10-07 | **Status:** ACCEPTED
|
|
67
|
+
- **Context:** `validateTarget` allowed any non-empty directory when `force` was true, with exactly one exception (the template directory itself). So `azcodr ~ --force` would merge the template into the user's home directory, `azcodr / --force` into the filesystem root, and `azcodr D:\projects --force` into the template's own parent workspace. The `--force` confirmation prompt ("is not empty (N items), Continue?") never named the ~12 paths about to be destroyed, and there was no home/root/ancestor guard at all. `assertInside` could not help: it is a lexical containment check, and `~/docs` *is* lexically inside `~` — the problem is the root chosen, not an escape from it.
|
|
68
|
+
- **Decision:** `isProtectedTarget()` refuses the filesystem root, the home directory, the home directory's parent (scaffolding into `/home` or `C:\Users` affects every user), ancestors of the template directory, and symlinks resolving to any of those (lexical comparison alone is bypassable; `realpath` is consulted for existing targets, compared case-insensitively on Windows). The refusal holds even with `--force` and even in `--dry-run`: `--force` means "overwrite files in a project directory", not "overwrite my home directory". The CLI fails fast before the non-empty prompt so answering "y" to a protected location is impossible; the library throws `E_TARGET_IS_PROTECTED` with a message naming the remedy. Programmatic callers can pass `allowProtected: true`; the CLI never does.
|
|
69
|
+
- **Consequences:** ✅ The catastrophic `--force` paths are closed, locked by `tests/protected-target.test.js` (14 tests, including symlink-alias and fault-injection cases for the two defensive catches). ✅ Only the exact protected directories are blocked: `~/my-project` and any normal subdirectory still scaffold. ✅ New stable error code follows the existing contract (branch on `code`, never message). ⚠️ A user who genuinely wants a project AT `~` must pick a subdirectory; that friction is the point.
|
|
70
|
+
- **Enforced In:** `lib/scaffold.js` (`isProtectedTarget`, `validateTarget`), `bin/azcodr.js` (fail-fast), `lib/index.d.ts`, `tests/protected-target.test.js`, `tests/error-codes.test.js`, `README.md` (security notes)
|
|
71
|
+
|
|
72
|
+
#### ADR-009: memory.md Is Append-Only; the Scaffolder Never Rewrites the Ledger
|
|
73
|
+
- **Date:** 2026-10-07 | **Status:** ACCEPTED
|
|
74
|
+
- **Context:** `.agents/skills/lets-build/scripts/bootstrap_workspace.sh` overwrote `memory.md` with a heredoc whenever the file contained any ADR heading that was not the untouched "No decisions recorded yet" placeholder. Reproduced directly: a project whose ADR read "we will never drop SQLite support" was silently replaced with an empty template after one `/lets-build` run — no error, no backup. It also fired on *high* numbering, so a mature project's ledger was equally destroyed. The intent was to strip template ADRs leaked from this repo; the trigger was the presence of real ones. ADRs are immutable history by rule (`docs/rules/agentic_configuration.md`), so a scaffolder rewriting them was a category error. Two further determinism defects in the same script: Topologies D (Canvas Game) and F (Systems Library) were documented in `SKILL.md` but had no `case` arm, so they fell through to a generic tree and **exited 0** — a wrong-but-successful scaffold the agent would report as done; and the `$LANGUAGE` argument was echoed and never used, so every language produced an identical tree while the skill claimed determinism.
|
|
75
|
+
- **Decision:** `memory.md` is never rewritten automatically. Recorded ADRs are detected by an anchored `^####\s+ADR-\d+` match and reported as preserved. Reset requires an explicit `AZCODR_RESET_MEMORY=1`, and even then writes `memory.md.bak` first and announces the action on stderr. Unknown topology and unknown language values are rejected with exit 2 and a list of valid values, instead of silently falling back. The decided profile is recorded to `.azcodr/workspace-profile.env` so downstream phases read it rather than re-deriving it.
|
|
76
|
+
- **Consequences:** ✅ The data-loss path is closed, and `tests/bootstrap-scripts.test.js` proves it: 4 of its tests fail against the prior destructive logic and pass against the fix, so it is a real guard rather than a passing assertion. ✅ A wrong topology can no longer masquerade as success. ✅ A mistyped language argument is caught immediately instead of silently ignored. ⚠️ `AZCODR_RESET_MEMORY=1` remains a genuinely destructive option by design; the backup plus announcement make it recoverable and visible rather than silent.
|
|
77
|
+
- **Enforced In:** `.agents/skills/lets-build/scripts/bootstrap_workspace.sh`, `tests/bootstrap-scripts.test.js`, `tests/shell-scripts.test.js`
|
|
78
|
+
|
|
79
|
+
#### ADR-008: JSR Publishing Blocked Pending an ESM Port
|
|
80
|
+
- **Date:** 2026-10-07 | **Status:** PROPOSED
|
|
81
|
+
- **Context:** Publishing to both npm and JSR was requested. npm is straightforward. JSR is not: its published-rules state *"ESM modules only... You cannot publish CommonJS modules."* This package is CommonJS (`module.exports`) with a hand-written `lib/index.d.ts`. An ESM shim re-exporting the CJS implementation was built and **verified to pass `jsr publish --dry-run`**, which is exactly the danger: a green dry-run is not proof of a working package. JSR rewrites relative specifiers at publish time and consumers run under Deno, where the CJS `require`/`module.exports` the shim depends on do not exist. Shipping it would produce a package that publishes cleanly and then fails on import.
|
|
82
|
+
- **Decision:** Do **not** ship a CJS-to-ESM shim. A real JSR package needs the implementation authored as ESM. Two viable routes, both requiring authorial work beyond this pass: (a) convert `lib/` to ESM and have npm consume it via `"type": "module"` plus a CJS build for legacy consumers, or (b) maintain a thin Deno-side ESM implementation. The `publish-jsr` job is present in `.github/workflows/publish.yml` with `if: false` and a stated reason, so the pipeline shape exists and enabling it is a one-line change plus the port. Nothing is half-enabled.
|
|
83
|
+
- **Consequences:** ✅ npm publishing is complete and verified end-to-end (pack → install → scaffold → validate). ✅ The JSR job is wired and its blocker is documented in code, not only in a commit message. ✅ No package is published to JSR in a state that would break on import. ⚠️ **JSR is not publishing.** If JSR is a hard requirement rather than a nice-to-have, route (a) must be scheduled; that is a real refactor, not a config change.
|
|
84
|
+
- **Enforced In:** `.github/workflows/publish.yml` (`publish-jsr` job), this ADR
|
|
85
|
+
|
|
86
|
+
#### ADR-007: Publish From CI, Never From a Workstation
|
|
87
|
+
- **Date:** 2026-10-07 | **Status:** ACCEPTED
|
|
88
|
+
- **Context:** The repository had no publish surface at all — publishing was a manual `npm publish` from a laptop. That bypasses every gate, publishes whatever is in the working tree rather than a tagged commit, and cannot be reproduced. Reviews also showed the release path had never been exercised, so nothing proved the published artifact even works.
|
|
89
|
+
- **Decision:** `.github/workflows/publish.yml`, triggered by `release: published` with a `workflow_dispatch` dry-run input. The `verify` job re-runs lint, tests, coverage, and architecture validation; asserts `package.json` version matches the release tag; asserts the working tree is clean; inspects the tarball for the 9 paths the package promises; then **installs the packed tarball into a scratch project, runs the CLI, asserts the programmatic API surface and error-code contract, scaffolds a real project, and validates it.** `publish-npm` only runs after `verify` succeeds.
|
|
90
|
+
- **Consequences:** ✅ The published artifact is proven runnable, not merely constructible — the highest-value gap the reviews identified. ✅ A dirty tree or a tag/version mismatch fails before any upload. ✅ `permissions:` is least-privilege (`contents: read` + `id-token: write`); CI concurrency prevents two release runs racing one version. ✅ `--provenance` is now actually passed, which the repo declares metadata for but never used. ⚠️ Requires `NPM_TOKEN` as a repository secret; provenance also needs npm trusted publishing configured on the package for full attestation.
|
|
91
|
+
- **Enforced In:** `.github/workflows/publish.yml`, `package.json` (`prepublishOnly`), `tests/coverage-config.test.js`
|
|
92
|
+
|
|
93
|
+
#### ADR-006: The Validator Fails Closed on Degraded Input
|
|
94
|
+
- **Date:** 2026-10-07 | **Status:** ACCEPTED
|
|
95
|
+
- **Context:** An adversarial pass proved five ways to make the validator report `SUCCESS` with exit 0 on a genuinely broken workspace. (1) A single unreadable directory made `walk()` `catch { return; }`, so a 6000-level tree reported "0 broken links" — a green result derived from zero coverage. (2) Any malformed ADR heading (`###` instead of `####`) produced zero parsed entries, silently downgrading the ledger to "clean slate" and disabling phase 6 entirely — the exact rot phase 6 exists to catch. (3) Reference-style links, empty link text, nested-bracket text, and HTML `<a href>` were invisible to the link checker. (4) A 0-byte `AGENTS.md` and an empty `docs/rules/` both passed. (5) Skill `name:`/`description:` were matched against the whole file, so body prose satisfied them. Four uncaught-exception crash sites aborted every later phase.
|
|
96
|
+
- **Decision:** Every filesystem read goes through a reporting helper; unreadable inputs produce a verdict, never a throw. Phase 6 fails closed: a malformed or duplicated ADR heading is an error, and an index advertising an ADR with no entry is an error even when zero entries parse. The walker reports what it skipped and aborts loudly on stack exhaustion. The link checker handles inline, reference-style, and HTML links, strips titles/queries/fragments, and matches schemes case-insensitively. `AGENTS.md` must be non-empty, `docs/rules/` must contain at least one rule, and front-matter lookups are scoped to the front-matter block.
|
|
97
|
+
- **Consequences:** ✅ 27 hardening regression tests plus 46 branch-coverage tests lock each closed exploit in place. ✅ All five false negatives now fail with exit 1. ✅ False positives eliminated (link titles, angle brackets, query strings, uppercase schemes, protocol-relative URLs). ✅ Four crashes became verdicts; two more were found while testing (`AGENTS.md` and `memory.md` unguarded reads) and fixed. ⚠️ Branch coverage for `validate.js` sits at 98.05% / 97.56% functions rather than 100% because it is a filesystem auditor containing fault handlers this host cannot produce (symlink creation needs Developer Mode; a `RangeError` needs a pathological tree). The threshold is documented inline rather than silently relaxed.
|
|
98
|
+
- **Enforced In:** `scripts/validate.js`, `tests/validate-hardening.test.js`, `tests/validate-coverage-paths.test.js`, `tests/validate-faults.test.js`, `scripts/test_coverage.js`
|
|
99
|
+
|
|
100
|
+
#### ADR-005: Verify Hooks, Do Not Assume Them
|
|
101
|
+
- **Date:** 2026-10-07 | **Status:** ACCEPTED
|
|
102
|
+
- **Context:** `.agents/scripts/safety_guard.sh` was audited and three defects were proven by execution, not by reading. (1) It read only `"$*"` and never stdin — but harnesses deliver the tool call as JSON on stdin, so `{"tool_input":{"command":"rm -rf /"}}` was **allowed**. Under the real protocol the guard was a total no-op. (2) `rm -rf .` was allowed, which is worse for a developer than `rm -rf /`. (3) `git push origin main --force` was allowed because the pattern required `--force` immediately after `push`. Separately, with no `.gitattributes`, a CRLF checkout gives every shipped `.sh` a `0D` before its shebang newline, and `set -euo pipefail` fails at line 6 — every guardrail skipped. `bash -n` passes on CRLF, so it was invisible to lint. `verify_completion.sh` failed **open**: no `package.json`, or a script not named exactly `test`, meant it verified nothing and exited 0; it also never ran the coverage gate.
|
|
103
|
+
- **Decision:** `safety_guard.sh` reads both argv and stdin, preserves whitespace in the extracted payload (a stray space-strip turned `rm -rf /` into `rm-rf/` and defeated every pattern), and blocks a widened denylist covering destructive `rm` in all flag spellings, force/mirror push, SQL DDL/DML case-insensitively without requiring a trailing semicolon, disk operations, remote-code-execution pipes, and ad-hoc `npm publish`. `verify_completion.sh` discovers scripts by exact key via Node, runs lint and the coverage gate, and reports "nothing was verified" instead of exiting 0. `.gitattributes` forces LF for `*.sh` and `bin/*`.
|
|
104
|
+
- **Consequences:** ✅ 25/25 known-bypass payloads blocked, 19/19 legitimate commands allowed, stdin channel proven. ✅ 52 regression tests in `tests/safety-guard.test.js` mean these cannot silently rot again. ✅ CI now runs `bash -n` over every shipped script and fails on CRLF. ✅ The guard **fails open** on unparseable input by design: a guard that locks an agent out of its own toolchain gets switched off, which is worse than no guard; the deterministic denylist is the compensating control.
|
|
105
|
+
- **Enforced In:** `.agents/scripts/safety_guard.sh`, `.agents/scripts/verify_completion.sh`, `.gitattributes`, `.github/workflows/ci.yml` (`shell-syntax` job), `tests/safety-guard.test.js`
|
|
106
|
+
|
|
107
|
+
#### ADR-004: Guard the Self-Healing Parity Repair Against Case-Insensitive Filesystems
|
|
108
|
+
- **Date:** 2026-10-07 | **Status:** ACCEPTED
|
|
109
|
+
- **Context:** Writing the new tests destroyed this repository's own `AGENTS.md` — 12,815 bytes replaced by the 10-byte string `AGENTS.md`. The parity repair writes `agents.md` when a directory listing lacks that exact entry. On a case-insensitive filesystem (Windows, macOS) `agents.md` and `AGENTS.md` are the **same file**, so an unguarded write destroys the very file the validator exists to protect. The guard was lost during an unrelated restructure, and the pre-existing test suite could not catch it because it never asserted that validation was non-destructive.
|
|
110
|
+
- **Decision:** Before repairing, check `fs.existsSync(lowerPath)`. On a case-insensitive filesystem that resolves true, meaning the invariant already holds; report satisfaction and write nothing. Repair only when the lower-cased path genuinely does not exist (a true case-sensitive filesystem, where the two paths are distinct entries).
|
|
111
|
+
- **Consequences:** ✅ `AGENTS.md` survives validation on every filesystem. ✅ Regression test asserts byte-identity of `AGENTS.md` after a validation run. ✅ The test was verified to fail against the reverted fix, so it is a real guard rather than a passing assertion. ✅ The lesson generalizes: **a self-healing tool must assert it left its inputs untouched.** Any repair path that writes must be paired with a before/after invariant test.
|
|
112
|
+
- **Enforced In:** `scripts/validate.js`, `tests/validate-adr.test.js`
|
|
113
|
+
|
|
114
|
+
#### ADR-003: Give Every Throw Site a Stable Error Code
|
|
115
|
+
- **Date:** 2026-10-07 | **Status:** ACCEPTED
|
|
116
|
+
- **Context:** All five `throw new Error(...)` sites in `lib/scaffold.js` were message-only, so consumers could only branch on message text — brittle across wording changes, and the exact anti-pattern typed-settings' `docs/errors.md` warns users against. `lib/index.d.ts` also omitted three runtime exports (`isInsideGitWorkTree`, `runGit`, `assertInside`).
|
|
117
|
+
- **Decision:** Introduce `ScaffoldError` carrying a `code` from a documented `ERROR_CODES` set (`E_TARGET_IS_TEMPLATE`, `E_TARGET_NOT_EMPTY`, `E_GIT_ARGS_INVALID`, `E_GIT_BLOCKED`, `E_PATH_ESCAPE`). Messages stay human-readable and actionable. Declare every code in the `.d.ts`; a test asserts the declarations match the runtime.
|
|
118
|
+
- **Consequences:** ✅ Machine-readable failure contract; message wording is now free to change. ✅ Type declarations match the real export surface. ✅ Test asserts all codes are distinct.
|
|
119
|
+
- **Enforced In:** `lib/scaffold.js`, `lib/index.d.ts`, `tests/error-codes.test.js`, `tests/architecture.test.js`
|
package/package.json
CHANGED
|
@@ -1,62 +1,65 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "azcodr",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Enterprise Architecture & Agentic Engineering Starter Template",
|
|
5
|
-
"bin": {
|
|
6
|
-
"azcodr": "bin/azcodr.js"
|
|
7
|
-
},
|
|
8
|
-
"main": "./lib/index.js",
|
|
9
|
-
"types": "./lib/index.d.ts",
|
|
10
|
-
"exports": {
|
|
11
|
-
".": {
|
|
12
|
-
"types": "./lib/index.d.ts",
|
|
13
|
-
"default": "./lib/index.js"
|
|
14
|
-
},
|
|
15
|
-
"./package.json": "./package.json"
|
|
16
|
-
},
|
|
17
|
-
"files": [
|
|
18
|
-
"bin",
|
|
19
|
-
"lib",
|
|
20
|
-
"scripts",
|
|
21
|
-
"data",
|
|
22
|
-
".github",
|
|
23
|
-
"AGENTS.md",
|
|
24
|
-
"memory.md",
|
|
25
|
-
"README.md",
|
|
26
|
-
"docs",
|
|
27
|
-
".agents",
|
|
28
|
-
".gitignore",
|
|
29
|
-
".editorconfig",
|
|
30
|
-
"LICENSE"
|
|
31
|
-
],
|
|
32
|
-
"keywords": [
|
|
33
|
-
"starter-template",
|
|
34
|
-
"agentic",
|
|
35
|
-
"multi-tenant",
|
|
36
|
-
"enterprise-architecture",
|
|
37
|
-
"scaffolding",
|
|
38
|
-
"ai-coding",
|
|
39
|
-
"agents",
|
|
40
|
-
"npx"
|
|
41
|
-
],
|
|
42
|
-
"author": "Subodh Khanal <prosubodh+git@gmail.com>",
|
|
43
|
-
"license": "MIT",
|
|
44
|
-
"repository": {
|
|
45
|
-
"type": "git",
|
|
46
|
-
"url": "git+https://github.com/prosubodh/azcodr.git"
|
|
47
|
-
},
|
|
48
|
-
"bugs": {
|
|
49
|
-
"url": "https://github.com/prosubodh/azcodr/issues"
|
|
50
|
-
},
|
|
51
|
-
"homepage": "https://github.com/prosubodh/azcodr#readme",
|
|
52
|
-
"engines": {
|
|
53
|
-
"node": ">=
|
|
54
|
-
},
|
|
55
|
-
"scripts": {
|
|
56
|
-
"test": "node --test",
|
|
57
|
-
"test:coverage": "node scripts/test_coverage.js",
|
|
58
|
-
"lint": "
|
|
59
|
-
"validate": "node scripts/validate.js",
|
|
60
|
-
"prepublishOnly": "npm run lint && npm run test:coverage && npm run validate"
|
|
61
|
-
}
|
|
62
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "azcodr",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "Enterprise Architecture & Agentic Engineering Starter Template",
|
|
5
|
+
"bin": {
|
|
6
|
+
"azcodr": "bin/azcodr.js"
|
|
7
|
+
},
|
|
8
|
+
"main": "./lib/index.js",
|
|
9
|
+
"types": "./lib/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./lib/index.d.ts",
|
|
13
|
+
"default": "./lib/index.js"
|
|
14
|
+
},
|
|
15
|
+
"./package.json": "./package.json"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"bin",
|
|
19
|
+
"lib",
|
|
20
|
+
"scripts",
|
|
21
|
+
"data",
|
|
22
|
+
".github",
|
|
23
|
+
"AGENTS.md",
|
|
24
|
+
"memory.md",
|
|
25
|
+
"README.md",
|
|
26
|
+
"docs",
|
|
27
|
+
".agents",
|
|
28
|
+
".gitignore",
|
|
29
|
+
".editorconfig",
|
|
30
|
+
"LICENSE"
|
|
31
|
+
],
|
|
32
|
+
"keywords": [
|
|
33
|
+
"starter-template",
|
|
34
|
+
"agentic",
|
|
35
|
+
"multi-tenant",
|
|
36
|
+
"enterprise-architecture",
|
|
37
|
+
"scaffolding",
|
|
38
|
+
"ai-coding",
|
|
39
|
+
"agents",
|
|
40
|
+
"npx"
|
|
41
|
+
],
|
|
42
|
+
"author": "Subodh Khanal <prosubodh+git@gmail.com>",
|
|
43
|
+
"license": "MIT",
|
|
44
|
+
"repository": {
|
|
45
|
+
"type": "git",
|
|
46
|
+
"url": "git+https://github.com/prosubodh/azcodr.git"
|
|
47
|
+
},
|
|
48
|
+
"bugs": {
|
|
49
|
+
"url": "https://github.com/prosubodh/azcodr/issues"
|
|
50
|
+
},
|
|
51
|
+
"homepage": "https://github.com/prosubodh/azcodr#readme",
|
|
52
|
+
"engines": {
|
|
53
|
+
"node": ">=22.8.0"
|
|
54
|
+
},
|
|
55
|
+
"scripts": {
|
|
56
|
+
"test": "node --test",
|
|
57
|
+
"test:coverage": "node scripts/test_coverage.js",
|
|
58
|
+
"lint": "eslint .",
|
|
59
|
+
"validate": "node scripts/validate-cli.js",
|
|
60
|
+
"prepublishOnly": "npm run lint && npm run test:coverage && npm run validate"
|
|
61
|
+
},
|
|
62
|
+
"devDependencies": {
|
|
63
|
+
"eslint": "10.12.0"
|
|
64
|
+
}
|
|
65
|
+
}
|
package/scripts/test_coverage.js
CHANGED
|
@@ -1,38 +1,66 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
const { spawnSync } = require('node:child_process');
|
|
5
|
-
|
|
6
|
-
// Native threshold flags and include filtering were added in Node v22.8.0.
|
|
7
|
-
// Fail closed on older runtimes instead of silently passing without a gate.
|
|
8
|
-
const [major, minor] = process.versions.node.split('.').map(Number);
|
|
9
|
-
const supportsThresholds = major > 22 || (major === 22 && minor >= 8);
|
|
10
|
-
if (!supportsThresholds) {
|
|
11
|
-
console.error(
|
|
12
|
-
`test:coverage requires Node >=22.8.0 for
|
|
13
|
-
`Upgrade Node or
|
|
14
|
-
);
|
|
15
|
-
process.exit(1);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const args = [
|
|
19
|
-
'--test',
|
|
20
|
-
'--experimental-test-coverage',
|
|
21
|
-
'--test-coverage-include=bin/**',
|
|
22
|
-
'--test-coverage-include=lib/**',
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { spawnSync } = require('node:child_process');
|
|
5
|
+
|
|
6
|
+
// Native threshold flags and include filtering were added in Node v22.8.0.
|
|
7
|
+
// Fail closed on older runtimes instead of silently passing without a gate.
|
|
8
|
+
const [major, minor] = process.versions.node.split('.').map(Number);
|
|
9
|
+
const supportsThresholds = major > 22 || (major === 22 && minor >= 8);
|
|
10
|
+
if (!supportsThresholds) {
|
|
11
|
+
console.error(
|
|
12
|
+
`test:coverage requires Node >=22.8.0 for threshold enforcement (current: ${process.versions.node}). ` +
|
|
13
|
+
`Upgrade Node, or invoke the runner directly: node --test --experimental-test-coverage.`
|
|
14
|
+
);
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const args = [
|
|
19
|
+
'--test',
|
|
20
|
+
'--experimental-test-coverage',
|
|
21
|
+
'--test-coverage-include=bin/**',
|
|
22
|
+
'--test-coverage-include=lib/**',
|
|
23
|
+
// Explicit file, not scripts/**: validate-cli.js is a two-line process entry
|
|
24
|
+
// that only runs when invoked directly, so it cannot be covered in-process.
|
|
25
|
+
// Every other script IS gated.
|
|
26
|
+
'--test-coverage-include=scripts/validate.js',
|
|
27
|
+
'--test-coverage-include=scripts/validate/io.js',
|
|
28
|
+
'--test-coverage-include=scripts/validate/text.js',
|
|
29
|
+
'--test-coverage-include=scripts/validate/parity.js',
|
|
30
|
+
'--test-coverage-include=scripts/validate/adr.js',
|
|
31
|
+
'--test-coverage-include=scripts/validate/root.js',
|
|
32
|
+
'--test-coverage-include=scripts/validate/rules.js',
|
|
33
|
+
'--test-coverage-include=scripts/validate/skills.js',
|
|
34
|
+
'--test-coverage-include=scripts/validate/links.js',
|
|
35
|
+
'--test-coverage-include=scripts/test_coverage.js',
|
|
36
|
+
// bin/, lib/ and test_coverage.js hold no environment-dependent defensive
|
|
37
|
+
// branches and are held at 100%. validate.js is a filesystem auditor: it
|
|
38
|
+
// contains fault handlers for conditions this host cannot produce (creating
|
|
39
|
+
// symlinks requires Developer Mode; a RangeError needs a tree thousands of
|
|
40
|
+
// levels deep). Those are exercised by tests/validate-faults.test.js where
|
|
41
|
+
// the fault can be injected, so the gate here is deliberately lower rather
|
|
42
|
+
// than silently passing.
|
|
43
|
+
'--test-coverage-branches=98',
|
|
44
|
+
'--test-coverage-functions=97',
|
|
45
|
+
'--test-coverage-lines=100'
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
const result = spawnSync(process.execPath, args, {
|
|
49
|
+
stdio: 'inherit',
|
|
50
|
+
env: process.env
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
if (result.error) {
|
|
54
|
+
console.error(result.error);
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// A null status means the child was killed by a signal (OOM, SIGKILL from a
|
|
59
|
+
// runner timeout). `status ?? 0` reported success for a crashed runner, which
|
|
60
|
+
// is how a green build can carry zero coverage data.
|
|
61
|
+
if (result.signal) {
|
|
62
|
+
console.error(`test runner terminated by signal ${result.signal}; coverage gate could not be evaluated.`);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
process.exit(result.status ?? 1);
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const { readTextOrFail } = require('./io.js');
|
|
6
|
+
const { stripHtmlComments, stripFencedCode } = require('./text.js');
|
|
7
|
+
|
|
8
|
+
function parseAdrLedger(memoryText) {
|
|
9
|
+
const withoutComments = stripFencedCode(stripHtmlComments(memoryText));
|
|
10
|
+
|
|
11
|
+
const entryIds = [...withoutComments.matchAll(/^####\s+ADR-(\d+)/gm)].map((m) => Number(m[1]));
|
|
12
|
+
|
|
13
|
+
// Any heading-ish line mentioning an ADR that is NOT a well-formed h4 entry.
|
|
14
|
+
// Catches the escapes that would otherwise silently downgrade the ledger to
|
|
15
|
+
// "clean slate": wrong heading depth, blockquoted, and missing space.
|
|
16
|
+
const malformed = [...withoutComments.matchAll(/^[^|\n]*#{1,6}[ \t]*\S*ADR-(\d+)/gim)]
|
|
17
|
+
.map((m) => Number(m[1]))
|
|
18
|
+
.filter((id) => !entryIds.includes(id));
|
|
19
|
+
|
|
20
|
+
const indexIds = [];
|
|
21
|
+
const tableRows = withoutComments.split('\n').filter((line) => /^\s*\|/.test(line));
|
|
22
|
+
for (const row of tableRows) {
|
|
23
|
+
// Skip the header and separator rows.
|
|
24
|
+
if (/^\s*\|[\s|:-]+\|\s*$/.test(row)) continue;
|
|
25
|
+
// cells[1] always exists: the row filter above guarantees a leading pipe,
|
|
26
|
+
// so splitting on '|' yields at least two cells.
|
|
27
|
+
const first = row.split('|')[1];
|
|
28
|
+
const m = first.match(/ADR-(\d+)/);
|
|
29
|
+
if (m) indexIds.push(Number(m[1]));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const duplicateIds = entryIds.filter((id, i) => entryIds.indexOf(id) !== i);
|
|
33
|
+
|
|
34
|
+
return { entryIds, indexIds, malformed, duplicateIds: [...new Set(duplicateIds)] };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Match the ledger's own ADR-001 zero-padded convention so messages can be
|
|
38
|
+
// grepped back to the offending heading.
|
|
39
|
+
function ledgerLabel(id) {
|
|
40
|
+
return `ADR-${String(id).padStart(3, '0')}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function reportMalformedHeadings(parsed, fail) {
|
|
44
|
+
for (const id of parsed.malformed) {
|
|
45
|
+
fail(
|
|
46
|
+
`${ledgerLabel(id)} appears in memory.md with a non-standard heading. ` +
|
|
47
|
+
'ADR entries must use exactly "#### ADR-NNN: Title" at h4.'
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
for (const id of parsed.duplicateIds) {
|
|
51
|
+
fail(`${ledgerLabel(id)} has more than one entry heading in memory.md.`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function reportIndexDrift(parsed, fail) {
|
|
56
|
+
const indexed = new Set(parsed.indexIds);
|
|
57
|
+
const missingFromIndex = parsed.entryIds.filter((id) => !indexed.has(id));
|
|
58
|
+
const missingEntry = parsed.indexIds.filter((id) => !parsed.entryIds.includes(id));
|
|
59
|
+
for (const id of missingFromIndex) {
|
|
60
|
+
fail(`${ledgerLabel(id)} exists in memory.md but is missing from the ADR Master Index table.`);
|
|
61
|
+
}
|
|
62
|
+
for (const id of missingEntry) {
|
|
63
|
+
fail(`ADR Master Index lists ${ledgerLabel(id)} but no matching entry exists in memory.md.`);
|
|
64
|
+
}
|
|
65
|
+
return missingFromIndex.length === 0 && missingEntry.length === 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function isLedgerClean(parsed, driftClean) {
|
|
69
|
+
return driftClean && parsed.malformed.length === 0 && parsed.duplicateIds.length === 0;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Fails when an ADR exists but is absent from the Master Index table, or when
|
|
74
|
+
* the index advertises an ADR that has no entry. Returns 'indexed' when there
|
|
75
|
+
* is at least one ADR entry to cross-check, otherwise 'clean-slate'.
|
|
76
|
+
*/
|
|
77
|
+
function checkAdrIndexConsistency(workspaceRoot, fail, pass) {
|
|
78
|
+
const memoryFile = path.join(workspaceRoot, 'memory.md');
|
|
79
|
+
if (!fs.existsSync(memoryFile)) return 'clean-slate';
|
|
80
|
+
const ledgerText = readTextOrFail(memoryFile, 'memory.md', fail);
|
|
81
|
+
if (ledgerText === null) return 'clean-slate';
|
|
82
|
+
|
|
83
|
+
const parsed = parseAdrLedger(ledgerText);
|
|
84
|
+
|
|
85
|
+
// A malformed heading must fail loudly. Previously any non-`####` heading
|
|
86
|
+
// produced zero entries, which silently downgraded the ledger to
|
|
87
|
+
// "clean slate" and disabled the entire phase.
|
|
88
|
+
reportMalformedHeadings(parsed, fail);
|
|
89
|
+
|
|
90
|
+
// Fail closed: an index that advertises an ADR must have a matching entry,
|
|
91
|
+
// even when no well-formed entry was parsed at all.
|
|
92
|
+
if (parsed.entryIds.length === 0 && parsed.indexIds.length === 0) {
|
|
93
|
+
return parsed.malformed.length === 0 ? 'clean-slate' : 'inconsistent';
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const driftClean = reportIndexDrift(parsed, fail);
|
|
97
|
+
if (isLedgerClean(parsed, driftClean)) {
|
|
98
|
+
pass(`ADR Master Index matches all ${parsed.entryIds.length} ADR entries in memory.md.`);
|
|
99
|
+
return 'indexed';
|
|
100
|
+
}
|
|
101
|
+
return 'inconsistent';
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The glossary is the second derived view that silently rots: an empty
|
|
106
|
+
* ubiquitous-language table alongside a populated ADR ledger means the DDD
|
|
107
|
+
* governance layer was scaffolded but never exercised.
|
|
108
|
+
*/
|
|
109
|
+
function checkGlossaryPopulated(workspaceRoot, verdicts) {
|
|
110
|
+
const { fail } = verdicts;
|
|
111
|
+
const glossary = path.join(workspaceRoot, 'docs', 'knowledge', 'ubiquitous_language.md');
|
|
112
|
+
if (!fs.existsSync(glossary)) {
|
|
113
|
+
fail('Missing docs/knowledge/ubiquitous_language.md (required once ADRs exist).');
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const raw = readTextOrFail(glossary, 'ubiquitous_language.md', fail);
|
|
117
|
+
if (raw === null) return;
|
|
118
|
+
checkGlossaryTerms(raw, verdicts, workspaceRoot);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function checkGlossaryTerms(raw, verdicts, workspaceRoot) {
|
|
122
|
+
const { fail } = verdicts;
|
|
123
|
+
// Explicit, greppable opt-out for the template repo itself. Its glossary is
|
|
124
|
+
// deliberately blank because it ships downstream as a starting point.
|
|
125
|
+
if (/azcodr:glossary-waived/.test(raw)) {
|
|
126
|
+
reportWaiverOutcome(workspaceRoot, verdicts.pass, verdicts.warn);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const withoutComments = raw.replace(/<!--[\s\S]*?-->/g, '');
|
|
130
|
+
const rows = withoutComments.split('\n').filter((line) => /^\s*\|/.test(line));
|
|
131
|
+
// Header + separator + one placeholder row = still a template.
|
|
132
|
+
const realTerms = rows.slice(2).filter((line) => !/No domain terms defined yet/i.test(line));
|
|
133
|
+
if (realTerms.length === 0) {
|
|
134
|
+
fail('ubiquitous_language.md has no domain terms, but memory.md records ADRs. Define the vocabulary during Domain Discovery.');
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function reportWaiverOutcome(workspaceRoot, pass, warn) {
|
|
139
|
+
const isTemplateItself = path.basename(workspaceRoot) === 'azcodr';
|
|
140
|
+
if (isTemplateItself) {
|
|
141
|
+
pass('ubiquitous_language.md is intentionally blank (template waiver).');
|
|
142
|
+
} else {
|
|
143
|
+
warn('ubiquitous_language.md carries the azcodr template waiver; delete the marker and define this project\'s vocabulary.');
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
module.exports = {
|
|
148
|
+
parseAdrLedger,
|
|
149
|
+
checkAdrIndexConsistency,
|
|
150
|
+
checkGlossaryPopulated
|
|
151
|
+
};
|