From 223e062dd3209f6f7c98d0569eaa36b16c5f8de3 Mon Sep 17 00:00:00 2001 From: Branimir Georgiev <66906831+braboj@users.noreply.github.com> Date: Fri, 30 Oct 2026 11:03:05 +0200 Subject: [PATCH 1/3] fix(emit): name no template file in what the emitter builds (#2414) An emitted project carries the rules, not the templates, so a template's path in an emitted skill or root file sent the agent to a file it cannot open. Seven template lines named another template's file; each now names the section by its heading, or drops the pointer where the file is not in every chain. SYS-27 emits every root, with skills and inlined, and fails on any line naming a template file; AGENTS.md states the authoring rule. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01DpT5xPB8YTAMW2Msp4aPTX --- AGENTS.md | 3 + docs/architecture/08-crosscutting-concepts.md | 78 +++++++++---------- docs/requirements.md | 1 + docs/tests/index.md | 1 + docs/tests/specs/SAIT-SMK-SYS-27-001A.md | 73 +++++++++++++++++ templates/backend/auth.md | 2 +- templates/backend/edge.md | 3 +- templates/core/agents.md | 2 +- templates/core/docs.md | 3 +- templates/core/quality-gates.md | 6 +- templates/core/review.md | 6 +- tests/smoke/registry.py | 5 +- tests/smoke/system.py | 73 +++++++++++++++++ 13 files changed, 204 insertions(+), 52 deletions(-) create mode 100644 docs/tests/specs/SAIT-SMK-SYS-27-001A.md diff --git a/AGENTS.md b/AGENTS.md index c18aeeb3..e7dfd036 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -151,6 +151,9 @@ labels are terminal, applied when closing without action: `duplicate` - A check that derives a comparison baseline by running a command MUST verify the command produced one and refuse when it did not — the refusal is a finding, never exit 3; see `base-quality-gates` +- Never name a template file's path in a template's prose: an emitted + project carries the rules, not the templates. Name the section by its + heading instead; SYS-27 checks every root's emitted files - Never name another file's section ID in running prose unless that file resolves into every chain carrying yours — state the substance inline instead; SYS-11 checks it per chain (spec, chapter 8.4) diff --git a/docs/architecture/08-crosscutting-concepts.md b/docs/architecture/08-crosscutting-concepts.md index a33aa1fd..046a8ca4 100644 --- a/docs/architecture/08-crosscutting-concepts.md +++ b/docs/architecture/08-crosscutting-concepts.md @@ -210,43 +210,43 @@ when their task starts (ADR-058). The root file counts its fact slots too. | Root | Every turn, words | Skills | Skill words | |------|-------------------|--------|-------------| -| `backend-api` | 5730 | 11 | 2721 | -| `backend-auth` | 7782 | 10 | 2101 | -| `backend-caching` | 5389 | 10 | 2101 | -| `backend-concurrency` | 5236 | 10 | 2101 | -| `backend-database` | 5196 | 11 | 2180 | -| `backend-edge` | 8118 | 10 | 2101 | -| `backend-errors` | 6635 | 10 | 2101 | -| `backend-features` | 5247 | 10 | 2101 | -| `backend-grpc` | 8894 | 11 | 2257 | -| `backend-http` | 5663 | 10 | 2101 | -| `backend-jobs` | 5169 | 10 | 2101 | -| `backend-messaging` | 5455 | 11 | 2195 | -| `backend-microservices` | 6852 | 11 | 2860 | -| `backend-monitoring` | 5528 | 10 | 2101 | -| `backend-observability` | 5275 | 10 | 2101 | -| `backend-webhooks` | 8637 | 11 | 2195 | -| `base-agents` | 4797 | 10 | 3194 | -| `base-cicd` | 5085 | 10 | 2221 | -| `base-config` | 5259 | 10 | 2131 | -| `base-containers` | 5138 | 10 | 2201 | -| `base-data-governance` | 5694 | 10 | 2101 | -| `base-data-migration` | 5367 | 11 | 2473 | -| `base-data-modeling` | 5206 | 10 | 2101 | -| `base-data-quality` | 5961 | 11 | 2150 | -| `base-deployment` | 5075 | 10 | 2160 | -| `base-devsecops` | 5383 | 10 | 2101 | -| `base-docs` | 4781 | 10 | 2101 | -| `base-embedded-checks` | 6035 | 11 | 4382 | -| `base-git` | 4781 | 10 | 2101 | -| `base-measurement` | 5508 | 10 | 2101 | -| `base-oop` | 5323 | 10 | 2101 | -| `base-quality` | 4781 | 10 | 2101 | -| `base-quality-gates` | 6014 | 11 | 4139 | -| `base-review` | 4781 | 10 | 2101 | -| `base-security` | 6312 | 10 | 2101 | -| `base-testing` | 4781 | 10 | 2101 | -| `frontend-content` | 5176 | 10 | 2101 | -| `frontend-quality` | 5201 | 10 | 2176 | -| `frontend-ux` | 5283 | 10 | 2101 | +| `backend-api` | 5730 | 11 | 2714 | +| `backend-auth` | 7784 | 10 | 2094 | +| `backend-caching` | 5389 | 10 | 2094 | +| `backend-concurrency` | 5236 | 10 | 2094 | +| `backend-database` | 5196 | 11 | 2173 | +| `backend-edge` | 8117 | 10 | 2094 | +| `backend-errors` | 6635 | 10 | 2094 | +| `backend-features` | 5247 | 10 | 2094 | +| `backend-grpc` | 8896 | 11 | 2250 | +| `backend-http` | 5663 | 10 | 2094 | +| `backend-jobs` | 5169 | 10 | 2094 | +| `backend-messaging` | 5455 | 11 | 2188 | +| `backend-microservices` | 6852 | 11 | 2853 | +| `backend-monitoring` | 5528 | 10 | 2094 | +| `backend-observability` | 5275 | 10 | 2094 | +| `backend-webhooks` | 8637 | 11 | 2188 | +| `base-agents` | 4797 | 10 | 3189 | +| `base-cicd` | 5085 | 10 | 2214 | +| `base-config` | 5259 | 10 | 2124 | +| `base-containers` | 5138 | 10 | 2194 | +| `base-data-governance` | 5694 | 10 | 2094 | +| `base-data-migration` | 5367 | 11 | 2466 | +| `base-data-modeling` | 5206 | 10 | 2094 | +| `base-data-quality` | 5961 | 11 | 2143 | +| `base-deployment` | 5075 | 10 | 2153 | +| `base-devsecops` | 5383 | 10 | 2094 | +| `base-docs` | 4781 | 10 | 2094 | +| `base-embedded-checks` | 6035 | 11 | 4377 | +| `base-git` | 4781 | 10 | 2094 | +| `base-measurement` | 5508 | 10 | 2094 | +| `base-oop` | 5323 | 10 | 2094 | +| `base-quality` | 4781 | 10 | 2094 | +| `base-quality-gates` | 6014 | 11 | 4134 | +| `base-review` | 4781 | 10 | 2094 | +| `base-security` | 6312 | 10 | 2094 | +| `base-testing` | 4781 | 10 | 2094 | +| `frontend-content` | 5176 | 10 | 2094 | +| `frontend-quality` | 5201 | 10 | 2169 | +| `frontend-ux` | 5283 | 10 | 2094 | diff --git a/docs/requirements.md b/docs/requirements.md index ae894010..2a75814a 100644 --- a/docs/requirements.md +++ b/docs/requirements.md @@ -69,6 +69,7 @@ Status: skeleton. Each cluster holds a few requirements until its fill | FR-06-04 | The system shall list each skill in the root file with the task that triggers it. | ADR-058 | SYS-26 | | FR-06-05 | The system shall reference a section marked `check` by its command and pass condition, never by copying the script body. | ADR-058 |SYS-26, partial, on a planted fixture | | FR-06-06 | The system shall build byte-identical files from the same input. | ADR-058 | SYS-26 | +| FR-06-07 | The system shall build files that name no template file, since an emitted project carries no templates. | ADR-058 | SYS-27 | ### 07. Shipped checks diff --git a/docs/tests/index.md b/docs/tests/index.md index 5ee3cad4..8658b251 100644 --- a/docs/tests/index.md +++ b/docs/tests/index.md @@ -50,6 +50,7 @@ restated in each. | `SAIT-SMK-SYS-24-001A` | SMK | P1 | A new section ID never splits a frozen section | | `SAIT-SMK-SYS-25-001A` | SMK | P1 | A load directive names a listed task, in its heading's tags | | `SAIT-SMK-SYS-26-001A` | SMK | P1 | The emitter places each section by its load, the same every run | +| `SAIT-SMK-SYS-27-001A` | SMK | P1 | An emitted file names no template file | | `SAIT-SMK-TPL-04-001A` | SMK | P1 | All EXTEND and OVERRIDE directives reference existing IDs | | `SAIT-INT-TPL-01-001A` | INT | P0 | DEPENDS ON chain assembles a complete rule set | | `SAIT-INT-TPL-02-001A` | INT | P1 | EXTEND adds rules without removing base rules | diff --git a/docs/tests/specs/SAIT-SMK-SYS-27-001A.md b/docs/tests/specs/SAIT-SMK-SYS-27-001A.md new file mode 100644 index 00000000..5e857343 --- /dev/null +++ b/docs/tests/specs/SAIT-SMK-SYS-27-001A.md @@ -0,0 +1,73 @@ +--- +id: SAIT-SMK-SYS-27-001A +title: An emitted file names no template file +product: sait +type: smoke +area: SYS +priority: p1 +status: ready +environment: [local, ci] +automatable: yes +created: 2026-10-30 +author: Branimir Georgiev +product-version: "3.x" +tags: [tooling, composition, delivery] +--- + +## Short description + +> **Given** every root a project can pick +> **When** `resolve_chain.py --emit` builds them, with skills and inlined +> **Then** no line of an emitted file names a template file, because an +> emitted project carries the rules and not the templates, so a named +> template is a file the agent cannot open and the rules it stands for +> never reach it + +## Results + +| Result | Condition | +|--------|-----------| +| PASSED | The planted template's two named paths are both found, and no file emitted from any root, in either mode, names a template file | +| FAILED | An emitted line names a template file, with or without its `templates/` prefix, or the planted scan misses a form | +| FAILED | The emitter exits non-zero | +| SKIPPED | — | +| BLOCKED | — | +| ERROR | — | + +## Steps + +### Prerequisites + +- Repository cloned locally +- Python 3 + +### Setup + +— (operates on a planted template and on every root, written to a +temporary directory that is removed afterwards) + +### Execution + +1. Build a planted template that names its own path once with the + `templates/` prefix, in a skill, and once without, in the root file +2. Run `resolve_chain.py --emit` on every root `--roots` lists, then again + with `--inline-skills` +3. Scan every emitted line for the path of every template file + +### Assertions + +1. Assert the scan finds both of the planted template's named paths +2. Assert no line emitted in either mode names a template file + +### Negative controls + +1. **The prefix-less form unread.** With the scan matching only paths that + start `templates/`, the planted case finds one of its two paths and + fails +2. **A pointer left in a template.** With the templates before this check + landed, the scan fails on the seven lines that named another template + +## Related + +- ADR-058: templates place each rule, and task procedures load on demand +- SYS-26: the emitter places each section by its load diff --git a/templates/backend/auth.md b/templates/backend/auth.md index 781e3d66..af2221a3 100644 --- a/templates/backend/auth.md +++ b/templates/backend/auth.md @@ -4,7 +4,7 @@ Rules for identity verification (authn) and access control (authz). Applies to any backend service that has protected resources. -Extends `security/security.md`, "Authentication" (`security-authn`) and +Extends the security rules "Authentication" (`security-authn`) and "Session management" (`security-sessions`), with backend-specific depth. ## General principles diff --git a/templates/backend/edge.md b/templates/backend/edge.md index c7718899..f4f74f77 100644 --- a/templates/backend/edge.md +++ b/templates/backend/edge.md @@ -27,8 +27,7 @@ mesh follows the microservices rules instead. front of it; an over-broad list lets callers spoof their IP and dodge rate limits - Propagate or originate a correlation/request ID at the edge so a - request is traceable end to end (see `backend/observability.md`, - "Distributed tracing") + request is traceable end to end (see "Distributed tracing") ## Routing and upstreams diff --git a/templates/core/agents.md b/templates/core/agents.md index 342aba8c..99c6ae95 100644 --- a/templates/core/agents.md +++ b/templates/core/agents.md @@ -129,7 +129,7 @@ not a note. ### 3.1 Testing ## 4. Review process -[Priority order and checklists from templates/core/review.md] +[Priority order and checklists from the review rules] ## 5. Session protocol [How a session starts, proceeds and ends] diff --git a/templates/core/docs.md b/templates/core/docs.md index 7f21e3fa..c7c17570 100644 --- a/templates/core/docs.md +++ b/templates/core/docs.md @@ -22,8 +22,7 @@ Which documents a project keeps, and how they stay true. [LOAD: write-docs] -- Carry these three in every project (MUST); their layout is the project's - own, and `templates/core/agents.md` offers a default for the agent files +- Carry these three in every project (MUST); their layout is the project's own | File | Purpose | | --- | --- | diff --git a/templates/core/quality-gates.md b/templates/core/quality-gates.md index dd51d420..f39c38db 100644 --- a/templates/core/quality-gates.md +++ b/templates/core/quality-gates.md @@ -49,8 +49,8 @@ the limit, and the check then exits non-zero. Committed generated artifacts — rendered docs, configs, resolved chains, schema output — drift silently without a gate. -- Wire the `--check` invocation from `core/docs.md`, "Generated files", - into CI as a required status check on the relevant paths +- Wire the `--check` invocation from "Generated files" into CI as a + required status check on the relevant paths - Generate hand-written content that reproduces tool output, such as a worked example, behind a marker, so the gate covers it - Enumerate the derivable blocks inside each artifact, not only the @@ -98,7 +98,7 @@ informally. - Run a check whose subject is the change under review in the environment that builds its tree - Report what a check inspected, not only what it found; the test-suite - form of this rule is `core/testing.md`, "Empty results" + form of this rule is "Empty results", in the testing rules - Report how many checks a tool could not read beside how many it ran (MUST) - Report every finding before exiting; where a check cannot continue past diff --git a/templates/core/review.md b/templates/core/review.md index 22032164..4744b39a 100644 --- a/templates/core/review.md +++ b/templates/core/review.md @@ -16,9 +16,9 @@ How a change is reviewed, and how a finding is proven before it is reported. [LOAD: review] -Review in this order, most critical first. `templates/core/quality.md`, and -any language quality template the project carries, is the standard for -items 2 to 4. +Review in this order, most critical first. The quality rules, and any +language quality rules the project carries, are the standard for items 2 +to 4. 1. **Security**: anything exploitable, any credential or licence problem 2. **Correctness**: wrong results, unhandled failures, race conditions diff --git a/tests/smoke/registry.py b/tests/smoke/registry.py index 27a42492..e8aaea86 100644 --- a/tests/smoke/registry.py +++ b/tests/smoke/registry.py @@ -11,7 +11,7 @@ check_sys_07, check_sys_09, check_sys_10, check_sys_11, check_sys_13, check_sys_14, check_sys_15, check_sys_16, check_sys_17, check_sys_18, check_sys_19, check_sys_20, check_sys_21, check_sys_22, check_sys_24, - check_sys_25, check_sys_26) + check_sys_25, check_sys_26, check_sys_27) from .templates import ( check_tpl_01, check_tpl_02, check_tpl_03, check_tpl_04, check_tpl_06, check_tpl_07, check_tpl_08, check_tpl_09, check_tpl_10) @@ -120,4 +120,7 @@ {"id": "SYS-26", "spec": "SAIT-SMK-SYS-26-001A", "title": "The emitter places each section by its load, the same every run", "fn": check_sys_26}, + {"id": "SYS-27", "spec": "SAIT-SMK-SYS-27-001A", + "title": "An emitted file names no template file", + "fn": check_sys_27}, ] diff --git a/tests/smoke/system.py b/tests/smoke/system.py index 0cbeaf9a..6986b136 100644 --- a/tests/smoke/system.py +++ b/tests/smoke/system.py @@ -2186,3 +2186,76 @@ def check_sys_26(): if trees[0][k] != trees[1][k]}) failures.append(f" two runs differ in: {', '.join(differ)}") return seen.failures() + failures, seen.notes() + + +# --------------------------------------------------------------------------- +# SYS-27 -- an emitted file names no template file +# --------------------------------------------------------------------------- + +# An emitted project carries the rules, not the templates they came from, so +# a template's path in an emitted file sends the agent to a file it cannot +# open, and the rules that line calls the standard never reach it. Every +# root is emitted with skills and inlined, and no line may name a template +# file, with or without its `templates/` prefix. A planted template naming +# itself both ways proves the scan finds each form. + +PATH_TEMPLATE = ("# Base — Demo\n[ID: base-demo]\n\n## Review\n[LOAD: release]\n\n" + "- `templates/core/demo.md` is the standard\n\n" + "## Naming\n\n- See `core/demo.md`, \"Naming\"\n") + + +def path_findings(emitted, paths): + """Each emitted file and line that names one of `paths`, a template's + path under the repository.""" + patterns = [(path, re.compile( + r"(? Date: Fri, 30 Oct 2026 11:04:11 +0200 Subject: [PATCH 2/3] docs(changelog): note the emitter's dead template paths Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01DpT5xPB8YTAMW2Msp4aPTX --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6527c756..5da05d33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -258,6 +258,8 @@ Releases up to 2.63.0 are listed in ### Fixed +- Emitted skills and root files no longer point to template files a + project does not carry ([#2415]) - Cached personal data follows its source's access and deletion rules instead of being banned ([#2370]) - The audit trail records that a PII field changed, not its values, so @@ -486,6 +488,7 @@ Releases up to 2.63.0 are listed in [#2386]: https://github.com/solid-ai-dev/solid-ai-templates/pull/2386 [#2387]: https://github.com/solid-ai-dev/solid-ai-templates/pull/2387 [#2389]: https://github.com/solid-ai-dev/solid-ai-templates/pull/2389 +[#2415]: https://github.com/solid-ai-dev/solid-ai-templates/pull/2415 ## [2.90.0] - 2026-09-12 From 7d0b12f68e7e8b1385fec552b6e6cd2e810c7ab9 Mon Sep 17 00:00:00 2001 From: Branimir Georgiev <66906831+braboj@users.noreply.github.com> Date: Fri, 30 Oct 2026 12:51:40 +0200 Subject: [PATCH 3/3] test(smoke): emit each root on its own in SYS-27 Built together, an override one root carries can drop the section another root names a template in, hiding the path. SYS-27 now builds every root alone, in both modes, and a planted override case proves the combined build hides a path the per-root build finds. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01DpT5xPB8YTAMW2Msp4aPTX --- docs/tests/specs/SAIT-SMK-SYS-27-001A.md | 36 +++++--- tests/smoke/system.py | 101 +++++++++++++++-------- 2 files changed, 90 insertions(+), 47 deletions(-) diff --git a/docs/tests/specs/SAIT-SMK-SYS-27-001A.md b/docs/tests/specs/SAIT-SMK-SYS-27-001A.md index 5e857343..60d393c9 100644 --- a/docs/tests/specs/SAIT-SMK-SYS-27-001A.md +++ b/docs/tests/specs/SAIT-SMK-SYS-27-001A.md @@ -17,19 +17,22 @@ tags: [tooling, composition, delivery] ## Short description > **Given** every root a project can pick -> **When** `resolve_chain.py --emit` builds them, with skills and inlined +> **When** the emitter builds each root on its own, with skills and inlined > **Then** no line of an emitted file names a template file, because an > emitted project carries the rules and not the templates, so a named > template is a file the agent cannot open and the rules it stands for > never reach it +Each root gets its own build because a project may pick it alone. Built +together, an override one root carries can drop the section another root +names a template in, and the path goes unseen. + ## Results | Result | Condition | |--------|-----------| -| PASSED | The planted template's two named paths are both found, and no file emitted from any root, in either mode, names a template file | +| PASSED | The planted template's two named paths are both found in each mode, the planted override hides one only when the roots are built together, and no file emitted from any root alone, in either mode, names a template file | | FAILED | An emitted line names a template file, with or without its `templates/` prefix, or the planted scan misses a form | -| FAILED | The emitter exits non-zero | | SKIPPED | — | | BLOCKED | — | | ERROR | — | @@ -43,21 +46,27 @@ tags: [tooling, composition, delivery] ### Setup -— (operates on a planted template and on every root, written to a -temporary directory that is removed afterwards) +— (operates on planted templates and on every root, built in memory; no +file is written) ### Execution 1. Build a planted template that names its own path once with the - `templates/` prefix, in a skill, and once without, in the root file -2. Run `resolve_chain.py --emit` on every root `--roots` lists, then again - with `--inline-skills` -3. Scan every emitted line for the path of every template file + `templates/` prefix, in a skill, and once without, in a section a + second planted root overrides +2. Build the planted roots together, then each on its own +3. Build every root `--roots` lists on its own, with skills and then with + skills inlined +4. Scan every emitted line for the path of every template file ### Assertions -1. Assert the scan finds both of the planted template's named paths -2. Assert no line emitted in either mode names a template file +1. Assert the scan finds both of the planted template's named paths, in + each mode +2. Assert the planted roots built together show one of the two paths, and + built each on its own show both +3. Assert no line emitted from any root, in either mode, names a template + file ### Negative controls @@ -65,7 +74,10 @@ temporary directory that is removed afterwards) start `templates/`, the planted case finds one of its two paths and fails 2. **A pointer left in a template.** With the templates before this check - landed, the scan fails on the seven lines that named another template + landed, the scan fails on the seven lines that named another template, + in every root that carries them +3. **Every root built together.** With the planted roots built as one + chain, the override hides the second path and the planted case fails ## Related diff --git a/tests/smoke/system.py b/tests/smoke/system.py index 6986b136..2778671d 100644 --- a/tests/smoke/system.py +++ b/tests/smoke/system.py @@ -16,7 +16,8 @@ from emit_context import build as emit_build from emit_context import write as emit_write from resolve_chain import ( - LOAD_CHECK, entry_sections, is_fixture, load_directives, parse_manifest, + LOAD_CHECK, entry_sections, is_fixture, load_catalogue, load_directives, + load_manifest, opt_in_roots, parse_manifest, read_file, resolve_chain, task_kinds) from testkit import ROOT, repository_files, template_files @@ -2195,13 +2196,25 @@ def check_sys_26(): # An emitted project carries the rules, not the templates they came from, so # a template's path in an emitted file sends the agent to a file it cannot # open, and the rules that line calls the standard never reach it. Every -# root is emitted with skills and inlined, and no line may name a template -# file, with or without its `templates/` prefix. A planted template naming -# itself both ways proves the scan finds each form. +# root is emitted on its own, with skills and inlined, and no line may name a +# template file, with or without its `templates/` prefix. Each root gets its +# own build because a project may pick one root alone: an override another +# root carries could drop the section that names the path, and a build of +# every root together would then hide it. A planted template naming itself +# both ways proves the scan finds each form, and a planted second root that +# overrides one of those sections proves each root is scanned on its own. PATH_TEMPLATE = ("# Base — Demo\n[ID: base-demo]\n\n## Review\n[LOAD: release]\n\n" "- `templates/core/demo.md` is the standard\n\n" - "## Naming\n\n- See `core/demo.md`, \"Naming\"\n") + "## Naming\n[ID: demo-naming]\n\n" + "- See `core/demo.md`, \"Naming\"\n") +PATH_STACK = ("# Stack — Demo\n\n## Naming\n[OVERRIDE: demo-naming]\n\n" + "- Name by role\n") +PATH_KINDS = {"release": "Preparing a release"} +PATH_CHAINS = { + "demo": [("templates/core/demo.md", PATH_TEMPLATE)], + "stack": [("templates/core/demo.md", PATH_TEMPLATE), + ("templates/fixture/stack.md", PATH_STACK)]} def path_findings(emitted, paths): @@ -2222,40 +2235,58 @@ def path_findings(emitted, paths): return findings +def root_path_findings(chains, kinds, paths, emitted_counts=None): + """Each root's emitted lines that name one of `paths`, the root built + alone with skills and inlined. `chains` maps a root to its (path, text) + pairs in resolution order; `emitted_counts`, a Counter, takes the files + emitted per mode.""" + findings = [] + for root in sorted(chains): + for inline in (False, True): + mode = "inline" if inline else "skills" + emitted = emit_build(chains[root], kinds, inline_skills=inline) + if emitted_counts is not None: + emitted_counts[mode] += len(emitted) + findings.extend(f"{root}, {mode}: {finding}" + for finding in path_findings(emitted, paths)) + return findings + + def check_sys_27(): seen = Inspected() failures = [] - planted = path_findings( - emit_build([("templates/core/demo.md", PATH_TEMPLATE)], - {"release": "Preparing a release"}), - ["templates/core/demo.md"]) - if len(planted) != 2: - failures.append(f" planted: {len(planted)} of the 2 named paths " - "found") - seen.count("planted paths found", len(planted)) - - resolver = os.path.join(ROOT, "tools", "resolve_chain.py") - roots = subprocess.run( - [sys.executable, resolver, "--roots"], capture_output=True, - text=True, encoding="utf-8").stdout.split() + planted = ["templates/core/demo.md"] + alone = root_path_findings({"demo": PATH_CHAINS["demo"]}, PATH_KINDS, + planted) + if len(alone) != 4: + failures.append(f" planted: {len(alone)} of the 4 named paths " + "found, 2 per mode") + seen.count("planted paths found", len(alone)) + + # Built together, the stack's override drops the Naming line and only + # the Review line is left; built per root, the demo root still has both. + together = path_findings( + emit_build(PATH_CHAINS["stack"], PATH_KINDS), planted) + split = root_path_findings(PATH_CHAINS, PATH_KINDS, planted) + if len(together) != 1 or len(split) != 6: + failures.append(f" planted override: {len(together)} found built " + f"together, owed 1; {len(split)} per root, owed 6") + + core_ids, entries, _ = load_manifest() + _, offered, catalogue = load_catalogue() + roots = ([stack["id"] for stack in catalogue] + + list(opt_in_roots(core_ids, offered, catalogue))) seen.count("roots emitted", len(roots)) paths = template_files() seen.count("template paths scanned for", len(paths)) - work = tempfile.mkdtemp(prefix="sait-emit-paths-") - try: - for mode in ([], ["--inline-skills"]): - out = os.path.join(work, "inline" if mode else "skills") - proc = subprocess.run( - [sys.executable, resolver, "--emit", out] + mode + roots, - capture_output=True, text=True, encoding="utf-8") - if proc.returncode != 0: - failures.append(f" emit exited {proc.returncode}: " - f"{(proc.stdout + proc.stderr).strip()}") - continue - tree = emitted_tree(out) - seen.count(f"files emitted, {os.path.basename(out)}", len(tree)) - failures.extend(f" {os.path.basename(out)}: {finding}" - for finding in path_findings(tree, paths)) - finally: - shutil.rmtree(work, ignore_errors=True) + chains = {root: [(path, read_file(path)) + for path in resolve_chain(root, core_ids, entries)] + for root in roots} + kinds = task_kinds(parse_manifest( + read(os.path.join(ROOT, "templates", "manifest.yaml")))) + emitted = Counter() + failures.extend(f" {finding}" for finding in + root_path_findings(chains, kinds, paths, emitted)) + for mode in ("skills", "inline"): + seen.count(f"files emitted, {mode}", emitted[mode]) return seen.failures() + failures, seen.notes()