arcane-cli 0.32.0 → 0.32.3

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.
@@ -8,19 +8,119 @@ tags: [cicd, pipelines, devops, prospero, azure-devops]
8
8
 
9
9
  # CI/CD Standards
10
10
 
11
- Pipeline patterns, branch policies, and deployment gates for all your projects. Owned by Prospero (DevOps Engineer).
11
+ Pipeline patterns, branch policies, and deployment gates for all your projects.
12
12
 
13
- ## Executive Summary
13
+ **Structure (ARC-038 decision 2):** a vendor-neutral core (principles that apply regardless of
14
+ which CI/CD platform a repository uses) plus a provider-specific profile — today, Azure DevOps,
15
+ the only platform this document details in full. This reuses the same shape
16
+ [[development-methodology|development-methodology.md]] already uses successfully for tracking
17
+ providers (Process-Template-Aware ADO Hierarchy Rules alongside GitHub Issues Conventions), not a
18
+ new architecture. A future GitHub-Actions- or GitLab-CI-specific profile can be added the same way,
19
+ without touching the core.
14
20
 
15
- - Every code repository must have a CI pipeline that runs on PR creation and merge to main. **Enforcement: explicitly advisory prose (ARC-023) — prescriptive guidance for repos Prospero provisions; Arcane's own tooling does not enumerate an organization's repositories to verify each one has a CI pipeline.**
16
- - Branch policies enforce PR requirements: build must pass, reviewer required, work item linked.
17
- - Terraform changes use plan-on-PR / apply-on-merge with manual approval gates.
18
- - Prospero owns pipeline creation and maintenance across all repos.
19
- - ADR-049 (Spell Loop) and ADR-051 (Prospero) provide the decision rationale.
21
+ ---
22
+
23
+ ## Core Principles (vendor-neutral)
24
+
25
+ Every code repository must have a CI pipeline that runs on PR creation and merge to main.
26
+ **Enforcement: explicitly advisory prose (ARC-023) — prescriptive guidance; Arcane's own tooling
27
+ does not enumerate an organization's repositories to verify each one has a CI pipeline.**
28
+
29
+ ### Branch Policy Principles
30
+
31
+ These apply to any platform's branch-protection mechanism (GitHub branch rulesets, Azure DevOps
32
+ branch policies, GitLab merge-request approval rules, ...) — the Azure DevOps Profile below shows
33
+ one concrete platform's implementation of the same table.
34
+
35
+ | Policy | Setting | Rationale |
36
+ |--------|---------|-----------|
37
+ | **Require PR** | All branches → main | No direct pushes to main. |
38
+ | **Minimum reviewers** | At least 1 | Quality gate. |
39
+ | **Build validation** | CI pipeline must pass | No merging broken code. |
40
+ | **Work item / issue linking** | Required (when ticketing is active) | Traceability. |
41
+ | **Comment resolution** | All comments must be resolved | No ignored feedback. |
42
+ | **Merge type** | Merge (no fast-forward) or Rebase and fast-forward — Squash disallowed | Preserves per-commit attribution trailers (ARC-009 §7); see [[git-conventions#Azure DevOps PR Merge Type]]. Enforcement: verified external platform policy (ARC-023) — `spell doctor`'s `checkPlatformBranchPolicy` (src/commands/doctor.ts, backed by src/modules/platform-policy.ts's evaluateGitHubMergePolicy/evaluateAdoMergePolicy) queries live GitHub Rulesets or Azure DevOps branch policy and verifies the effective merge methods match this ladder — a local, on-demand CLI check the operator runs, not a check wired into CI itself. |
43
+
44
+ **Enforcement (every row above except Merge type): explicitly advisory prose (ARC-023)** —
45
+ prescriptive for whatever platform a consumer repository uses; no Arcane check verifies these
46
+ policies are actually configured, on any platform.
47
+
48
+ For docs-only repos: a PR is not strictly required (a project may allow local ff-only merges for
49
+ pure documentation, per its own governance); a CI lint check can still run on push to any branch;
50
+ branch discipline (branch → work → ff-only merge) still applies regardless.
51
+
52
+ ### CI-Skip Semantics: Never Trust Commit Metadata (ARC-022)
53
+
54
+ CI skip/run decisions are based **only on changed paths** — never on commit message, author
55
+ identity, or branch name. Do not add a *new* mechanism (a custom `docs(...)`-style commit-prefix
56
+ convention, a bot rule, a pipeline-level check) that lets a commit opt itself out of validation by
57
+ what it *says* rather than what it *changed*. **Enforcement: explicitly advisory prose (ARC-023) —
58
+ prescriptive guidance for any consumer CI platform; this repo's own `.github/workflows/ci.yml` runs
59
+ unconditionally on every push/PR to `main` with no path filter at all, so it neither implements nor
60
+ is bound by this pattern, and no Arcane check inspects a downstream repo's CI configuration for
61
+ compliance.**
62
+
63
+ Commit messages, authorship, and branch names are attacker-controlled metadata — anyone who can
64
+ push a commit controls all three. A CI-skip mechanism keyed on any of them is a bypass primitive:
65
+ it lets a malicious or careless commit self-certify as "safe to skip" regardless of what it
66
+ actually touched. Path-based filtering is the only trust signal this document recognizes, because
67
+ the pipeline itself independently observes the changed paths rather than trusting a claim embedded
68
+ in the commit.
69
+
70
+ **Know your platform's default skip-comment behavior — don't assume this rule alone controls it.
71
+ Enforcement: explicitly advisory prose (ARC-023) — this paragraph discloses a residual
72
+ platform-level gap; it is not a mechanism Arcane enforces.** Most major CI platforms honor some
73
+ form of skip-comment convention in a pushed commit message by default (see the Azure DevOps
74
+ Profile below for the exact syntax that platform recognizes) — this is platform behavior, not
75
+ something a consumer opts into, and this governance rule cannot disable it from pipeline
76
+ configuration alone. The required-before-merge gate a repo's own branch policy relies on should be
77
+ verified to run *regardless* of any such skip-comment convention, so the merge gate itself stays
78
+ sound. What is generally **not** protected: any other trigger-based run a repo depends on for
79
+ signal outside that one merge gate — direct pushes to a non-PR branch, a secondary environment
80
+ build, a dashboard keyed off pipeline status. Treat those as a residual, platform-level exposure
81
+ this document can flag but not close; if a trigger-based run must be tamper-resistant, that
82
+ requires an organization-level policy outside any one repo's own pipeline configuration, not a
83
+ governance sentence.
84
+
85
+ ### Branch-Policy Path-Filter Alignment (ARC-022)
86
+
87
+ A pipeline's own trigger path filter and a branch-protection rule's **required-check path filter**
88
+ (where the platform supports scoping a required check to specific paths) are two independent
89
+ mechanisms, and only one of them controls whether a passing build is actually *required* before a
90
+ PR can merge. If the branch-protection path filter is narrower than the pipeline's own trigger
91
+ scope, a change that falls outside the protection filter's scope can merge with
92
+ **no build check required at all** — even though the pipeline itself is correctly scoped and would
93
+ have run and caught a problem, because nothing forced it to be a merge requirement for that
94
+ specific change.
95
+
96
+ Whenever a pipeline's own trigger scope changes, any path-scoped branch-protection requirement (if
97
+ the platform has one) must be reviewed and updated to match or be a strict superset — never left narrower.
98
+ **Enforcement: explicitly advisory prose (ARC-023) — prescriptive for any consumer
99
+ repository with path-scoped branch protection; Arcane has no visibility into a downstream repo's
100
+ branch-protection path-filter configuration and cannot verify the two stay aligned.** See the Azure
101
+ DevOps Profile below for a concrete failure mode observed on that platform.
102
+
103
+ ### Deployment Gate Principles
104
+
105
+ | Environment | Gate | Who Approves |
106
+ |-------------|------|-------------|
107
+ | **Staging** | CI passes + PR approved | Automatic on merge. |
108
+ | **Production** | Staging verified + manual approval | Human (operator). |
109
+ | **Infrastructure apply** (Terraform or equivalent) | Plan reviewed + security scan clean | Human (operator). |
110
+
111
+ **Enforcement: explicitly advisory prose (ARC-023)** — describes an intended gating shape for any
112
+ consumer pipeline; Arcane does not verify a downstream repo's environment or approval configuration
113
+ on any platform.
20
114
 
21
115
  ---
22
116
 
23
- ## Pipeline Matrix
117
+ ## Azure DevOps Profile
118
+
119
+ Everything below is specific to Azure DevOps — the only platform this document currently details
120
+ in full, applying the core principles above through that platform's actual mechanisms. Owned by
121
+ Prospero (DevOps Engineer) where a roster assigns that role.
122
+
123
+ ### Pipeline Matrix
24
124
 
25
125
  | Repo | Org | Pipeline | Triggers | Steps | Owner |
26
126
  |------|-----|----------|----------|-------|-------|
@@ -31,31 +131,25 @@ Pipeline patterns, branch policies, and deployment gates for all your projects.
31
131
  | storefront / web | {ado-org} | Build + Deploy | PR, merge to main | build → deploy (platform-dependent) | Prospero |
32
132
  | legacy-app / App | {ado-org} | Build + Test | PR, merge to main | dotnet restore → build → test | Prospero |
33
133
 
34
- ---
35
-
36
- ## Branch Policies (Azure DevOps)
134
+ ### Branch Policies (Azure DevOps mechanics)
37
135
 
38
- Applied to all code repositories (not docs-only repos per ADR-048):
136
+ Applies the Core Principles' branch-policy table above through Azure DevOps's own Branch Policies
137
+ screen (Project Settings → Repositories → Branch Policies), applied to all code repositories (not
138
+ docs-only repos per ADR-048): "Require PR" is Azure DevOps's own require-a-minimum-number-of-
139
+ reviewers policy; "Work item linking" maps to Azure Boards work items; "Build validation" is Azure
140
+ DevOps's own named Build Validation policy type. **Enforcement: explicitly advisory prose
141
+ (ARC-023) — prescriptive for consumer Azure DevOps repos specifically; no Arcane check verifies any
142
+ of these policies are configured there** (Merge type is the one exception — see the Core table's
143
+ own enforcement note, which already covers Azure DevOps).
39
144
 
40
- | Policy | Setting | Rationale |
41
- |--------|---------|-----------|
42
- | **Require PR** | All branches → main | No direct pushes to main. Enforcement: explicitly advisory prose (ARC-023) — prescriptive for consumer Azure DevOps repos; no Arcane check verifies a PR-required policy is configured. |
43
- | **Minimum reviewers** | 1 (Lince for code, Merlin for architecture) | Quality gate. Enforcement: explicitly advisory prose (ARC-023) — prescriptive for consumer Azure DevOps repos; no Arcane check verifies configured reviewer counts. |
44
- | **Build validation** | CI pipeline must pass | No merging broken code. Enforcement: explicitly advisory prose (ARC-023) — prescriptive for consumer Azure DevOps repos; no Arcane check verifies a build-validation policy is configured. |
45
- | **Work item linking** | Required (when ticketing active) | Traceability. Enforcement: explicitly advisory prose (ARC-023) — prescriptive for consumer Azure DevOps repos; no Arcane check verifies it. |
46
- | **Comment resolution** | All comments must be resolved | No ignored feedback. Enforcement: explicitly advisory prose (ARC-023) — prescriptive for consumer Azure DevOps repos; no Arcane check verifies a comment-resolution policy is configured. |
47
- | **Merge type** | Merge (no fast-forward) or Rebase and fast-forward — Squash disallowed | Preserves per-commit attribution trailers (ARC-009 §7); see [[git-conventions#Azure DevOps PR Merge Type]]. Enforcement: verified external platform policy (ARC-023) — `spell doctor`'s `checkPlatformBranchPolicy` (src/commands/doctor.ts, backed by src/modules/platform-policy.ts's evaluateGitHubMergePolicy/evaluateAdoMergePolicy) queries live GitHub Rulesets or Azure DevOps branch policy and verifies the effective merge methods match this ladder — a local, on-demand CLI check the operator runs, not a check wired into CI itself. |
48
-
49
- For docs-only repos (Arcane):
145
+ For docs-only repos (Arcane itself, when hosted on Azure DevOps):
50
146
  - PR not required (ADR-048 allows local ff-only merge for docs)
51
147
  - CI lint check runs on push to any branch
52
148
  - Branch discipline still applies (branch → work → ff-only merge)
53
149
 
54
- ---
150
+ ### Pipeline Templates
55
151
 
56
- ## Pipeline Templates
57
-
58
- ### .NET Backend Pipeline
152
+ #### .NET Backend Pipeline
59
153
 
60
154
  Fail-safe path filter (ARC-022): **exclude** known-inert doc paths rather than **include** a
61
155
  named code directory — a new code directory added outside `src/**` would silently never trigger
@@ -106,7 +200,7 @@ steps:
106
200
  failIfCoverageEmpty: true
107
201
  ```
108
202
 
109
- ### Node.js Pipeline
203
+ #### Node.js Pipeline
110
204
 
111
205
  Previously had no path filter at all — fail-safe (never misses a code change) but wasteful (a
112
206
  pure docs commit still burns a full pipeline run). Fail-safe path filter (ARC-022): the same
@@ -146,7 +240,7 @@ steps:
146
240
  condition: succeededOrFailed()
147
241
  ```
148
242
 
149
- ### Terraform Pipeline
243
+ #### Terraform Pipeline
150
244
 
151
245
  An **include** filter is correct here, not the ARC-022 anti-pattern — but only if it's a
152
246
  **filetype** glob, not a directory-prefix one. **Enforcement: explicitly advisory prose (ARC-023) — prescriptive for the Azure DevOps template this repo ships to consumers; Arcane has no shipped `azure-pipelines.terraform.yml` file of its own to check parity against, and cannot verify a downstream repo kept the include filter filetype-scoped rather than directory-prefixed.** An earlier version of this template included only
@@ -202,7 +296,7 @@ steps:
202
296
  # Apply step is manual — requires approval gate
203
297
  ```
204
298
 
205
- ### Markdown Lint Pipeline
299
+ #### Markdown Lint Pipeline
206
300
 
207
301
  Also correctly include-scoped, for the same reason as the Terraform pipeline: this pipeline
208
302
  exists specifically to lint markdown, not to validate code generally, and `**.md` already has the
@@ -237,67 +331,26 @@ steps:
237
331
  condition: succeededOrFailed()
238
332
  ```
239
333
 
240
- ---
241
-
242
- ## Never Trust Commit Metadata for CI Skipping (ARC-022)
243
-
244
- CI skip/run decisions are based **only on changed paths** — never on commit message, author
245
- identity, or branch name. Do not add a *new* mechanism (a custom `docs(...)`-style commit-prefix
246
- convention, a bot rule, a pipeline-level check) that lets a commit opt itself out of validation by
247
- what it *says* rather than what it *changed*. **Enforcement: explicitly advisory prose (ARC-023) — prescriptive guidance for Azure DevOps consumer pipelines; this repo's own `.github/workflows/ci.yml` runs unconditionally on every push/PR to `main` with no path filter at all, so it neither implements nor is bound by this pattern, and no Arcane check inspects a downstream repo's CI configuration for compliance.**
248
-
249
- Commit messages, authorship, and branch names are attacker-controlled metadata — anyone who can
250
- push a commit controls all three. A CI-skip mechanism keyed on any of them is a bypass primitive:
251
- it lets a malicious or careless commit self-certify as "safe to skip" regardless of what it
252
- actually touched. Path-based filtering (above) is the only trust signal this repository
253
- recognizes, because the pipeline itself independently observes the changed paths rather than
254
- trusting a claim embedded in the commit.
255
-
256
- **Know the platform default, don't assume this rule alone controls it. Enforcement: explicitly advisory prose (ARC-023) — this paragraph discloses a residual platform-level gap; it is not a mechanism Arcane enforces.** Azure Pipelines honors
257
- `[skip ci]`, `[ci skip]`, `skip-checks: true`, `[skip azurepipelines]`, `[skip azpipelines]`,
258
- `[skip azp]`, and `***NO_CI***` in a pushed commit message **by default**, on `trigger:`-driven
259
- runs — this is platform behavior, not something a consumer opts into, and this governance rule
260
- cannot disable it from the YAML alone. The required-before-merge gate this repo's own Branch
261
- Policies table relies on (**build validation on the PR's merge commit**) is documented to run
262
- *regardless* of `[skip ci]` and its variants, so the merge gate itself stays sound. What is **not**
263
- protected: any other trigger-based run this repo depends on for signal outside that one gate —
264
- direct pushes to a non-PR branch, a secondary environment build, a dashboard keyed off pipeline
265
- status. Treat those as a residual, platform-level exposure this document can flag but not close;
266
- if a trigger-based run must be tamper-resistant, that requires an organization-level policy
267
- outside this repo's own YAML, not a governance sentence.
268
-
269
- ## Branch-Policy Path-Filter Alignment (ARC-022)
270
-
271
- A YAML pipeline's own `trigger`/`pr` path filter and an Azure DevOps branch policy's **build
272
- validation path filter** are two independent mechanisms, and only one of them controls whether a
273
- passing build is actually *required* before a PR can merge. If the branch policy's path filter is
274
- narrower than the pipeline's own trigger scope, a change that falls outside the policy's filter
275
- can merge with **no build check required at all** — even though the pipeline itself is correctly
276
- scoped and would have run and caught a problem, because nothing forced it to be a merge
277
- requirement for that specific change.
278
-
279
- **Concrete failure mode:** the .NET pipeline above triggers on everything except docs (exclude
280
- filter, fail-safe). If the repo's branch policy still requires the build check only for changes
281
- under `paths: src/**` (an older include-style policy filter, or one configured before this repo
282
- adopted exclude-based YAML triggers), a change to a new top-level code directory outside `src/`
283
- triggers the pipeline (YAML is fail-safe) but does **not** require it to pass before merge (branch
284
- policy is not) — the build can run, fail, and be ignored, or the PR can merge before it finishes.
285
- Whenever a pipeline's YAML trigger scope changes, the branch policy's own path filter (if it has
286
- one) must be reviewed and updated to match or be a strict superset — never left narrower. **Enforcement: explicitly advisory prose (ARC-023) — prescriptive for consumer Azure DevOps projects; Arcane has no visibility into a downstream repo's branch-policy path-filter configuration and cannot verify the two stay aligned.**
334
+ ### Azure Pipelines' Skip-CI Syntax (the Core section's "know your platform's default")
287
335
 
288
- ---
336
+ Azure Pipelines honors `[skip ci]`, `[ci skip]`, `skip-checks: true`, `[skip azurepipelines]`,
337
+ `[skip azpipelines]`, `[skip azp]`, and `***NO_CI***` in a pushed commit message **by default**, on
338
+ `trigger:`-driven runs. The required-before-merge gate this document's own Branch Policies section
339
+ relies on (**build validation on the PR's merge commit**) is documented to run *regardless* of
340
+ `[skip ci]` and its variants, so the merge gate itself stays sound. What is **not** protected: any
341
+ other trigger-based run a repo depends on for signal outside that one gate — direct pushes to a
342
+ non-PR branch, a secondary environment build, a dashboard keyed off pipeline status.
289
343
 
290
- ## Deployment Gates
344
+ ### Branch-Policy Path-Filter Alignment: a concrete Azure DevOps failure mode
291
345
 
292
- | Environment | Gate | Who Approves |
293
- |-------------|------|-------------|
294
- | **Staging** | CI passes + PR approved | Automatic on merge. Enforcement: explicitly advisory prose (ARC-023) — describes a consumer Azure DevOps pipeline's intended gating; Arcane does not verify a downstream repo's environment configuration. |
295
- | **Production** | Staging verified + manual approval | Human (operator). Enforcement: explicitly advisory prose (ARC-023) — describes a consumer Azure DevOps environment's approval configuration; Arcane does not verify it is actually configured. |
296
- | **Terraform apply** | Plan reviewed + Checkov clean | Human (operator). Enforcement: explicitly advisory prose (ARC-023) — describes a consumer Azure DevOps pipeline's manual-apply step; Arcane does not verify it is actually configured. |
297
-
298
- ---
346
+ The .NET pipeline above triggers on everything except docs (exclude filter, fail-safe). If the
347
+ repo's branch policy still requires the build check only for changes under `paths: src/**` (an
348
+ older include-style policy filter, or one configured before this repo adopted exclude-based YAML
349
+ triggers), a change to a new top-level code directory outside `src/` triggers the pipeline (YAML is
350
+ fail-safe) but does **not** require it to pass before merge (branch policy is not) — the build can
351
+ run, fail, and be ignored, or the PR can merge before it finishes.
299
352
 
300
- ## Prospero's Responsibilities
353
+ ### Prospero's Responsibilities
301
354
 
302
355
  1. **Create and maintain all CI/CD pipelines** across Azure DevOps organizations
303
356
  2. **Configure branch policies** per repo risk level
@@ -308,9 +361,7 @@ one) must be reviewed and updated to match or be a strict superset — never lef
308
361
  7. **Deployment automation** — Azure Functions, App Service, container deployments
309
362
  8. **Documentation** — keep this file and pipeline configs in sync
310
363
 
311
- ---
312
-
313
- ## Implementation Priority
364
+ ### Implementation Priority
314
365
 
315
366
  1. WidgetApp .NET backend pipeline (highest business priority)
316
367
  2. ops docs lint pipeline (catches documentation drift)
@@ -319,6 +370,7 @@ one) must be reviewed and updated to match or be a strict superset — never lef
319
370
  5. Terraform pipeline (when new Azure resources are provisioned)
320
371
  6. AcmeStore pipeline (when platform is selected)
321
372
 
373
+ ---
322
374
 
323
375
  ## Related
324
376
 
@@ -189,14 +189,14 @@ Without this chain, agents make conflicting decisions across stories (e.g., one
189
189
 
190
190
  ### Adversarial Review
191
191
 
192
- `spell-review` requires the reviewer to **find issues**. "Looks good" is not an acceptable review. The reviewer must:
192
+ `spell-review` requires the reviewer to **cover every lens, not hit a finding count**. "Looks good" without addressing each lens is not an acceptable review. The reviewer must:
193
193
 
194
- 1. Find a minimum of 3 issues (or explain why fewer exist)
195
- 2. Classify each as HIGH / MEDIUM / LOW severity
194
+ 1. Explicitly cover every lens — correctness, security, performance, tests, naming/clarity, architecture — and state "no issues" for any lens that is clean. There is no finding quota: zero findings is a valid outcome on a clean or small diff; never fabricate issues to hit one.
195
+ 2. Classify each real finding as HIGH / MEDIUM / LOW severity
196
196
  3. Check for missing test coverage, architecture violations, security issues
197
197
  4. Defer unrelated findings to a backlog (don't derail the current change)
198
198
 
199
- **Enforcement: explicitly advisory prose (ARC-023) — no script counts or verifies findings, and `spell-review.prompt.md`'s current Rules section has superseded item 1's minimum with "no finding quota... zero findings is a valid outcome," so this minimum should be read as superseded framing, not current spell behavior.**
199
+ **Enforcement: explicitly advisory prose (ARC-023) — no script counts or verifies lens coverage; this text now matches `spell-review.prompt.md`'s own current Rules section (coverage of effort, not a count of findings) rather than the superseded "minimum of 3" framing an earlier version of this rule stated.**
200
200
 
201
201
  ### Append-Only Progress
202
202
 
@@ -339,7 +339,7 @@ A tool that writes code **must not also run `spell-review` on that same code. En
339
339
  ### Minimum gate for any Copilot-produced code
340
340
 
341
341
  ```
342
- Copilot implements → spell-test (run tests, check coverage) → spell-review (adversarial pass, min 3 findings) → spell-ship
342
+ Copilot implements → spell-test (run tests, check coverage) → spell-review (adversarial pass, every lens covered) → spell-ship
343
343
  ```
344
344
 
345
345
  This applies regardless of change size. Even single-file bug fixes must pass `spell-review` before being pushed to `origin/main`.
@@ -35,7 +35,7 @@ Paste this entire file into any AI client (Claude web, ChatGPT, etc.) to give it
35
35
  5. **Log significant decisions** in DECISIONS.md as ADRs.
36
36
  6. **Production system** — no shortcuts, no "fix it later."
37
37
  7. **Conventional Commits** format: `type(scope): description`.
38
- 8. **Never auto-commit** during interactive sessions — present for approval first.
38
+ 8. **Never auto-commit** during interactive sessions — present for approval first. Exception: autonomous agents at Magus+ power level may self-commit within approved scope.
39
39
  9. **Recommendation guardrails:** Flag actionable recommendations. Verify information. Present free alternatives. Confirm >$50 items.
40
40
 
41
41
  ## Key Files
@@ -50,7 +50,7 @@ this broader ≥ Silver gate is not fully verified by any coded scorer or workfl
50
50
  | --- | --- |
51
51
  | Bronze | No real organization, person, venture, or machine names. Org-specific values use `{UPPER_SNAKE}` placeholders. Enforcement: executable check (ARC-023) — `scripts/org-token-lint.ts` scans `.github/prompts/*.prompt.md` against the `ARCANE_ORG_TOKENS` denylist and runs automatically as part of `npm run build`, failing the build on a match; `spell ward --gate` (`src/commands/ward.ts`) runs the same denylist-scanning engine standalone but is not currently wired into CI as its own gate. |
52
52
  | Silver | + every placeholder has an inline resolution note: *"resolve from `.arcane.json` / frontmatter; ask if unset."* Enforcement: explicitly advisory prose (ARC-023) — part of the D2 hard gate by name, but this specific bar is self-graded; no check verifies a placeholder carries an inline resolution note. |
53
- | Gold | + no hard assumption of a specific tracker, agent roster, or directory layout — the spell works in a vanilla consuming repo with no Arcane context files present. |
53
+ | Gold | + no hard assumption of a specific tracker, CI/CD platform, cloud/deployment vendor, agent roster, or directory layout — the spell works in a vanilla consuming repo with no Arcane context files present. |
54
54
 
55
55
  > A **Bronze on D2 is OSS-blocking** and must be fixed before release. **Enforcement: executable check (ARC-023) — same mechanism as the D2 Bronze bar above: the `org-token-lint` build gate fails `npm run build` automatically; `spell ward --gate` provides the same scan standalone but is not yet wired into CI.** See **Distributability conventions** below.
56
56
 
@@ -136,6 +136,18 @@ Arcane spells ship to other repositories and, eventually, open source. Keep them
136
136
  [[development-methodology]] — point to them rather than re-inlining ADO logic. Enforcement: explicitly
137
137
  advisory prose (ARC-023) — self-graded during spell audit; no lint scans spell prompts for a
138
138
  hard-coded tracker assumption.
139
+ - **CI/CD and deployment vendors are optional too (ARC-038 decision 3):** the same D2 bar that
140
+ forbids assuming a single tracker applies to any other platform a spell might otherwise hard-code
141
+ — a CI/CD system, a cloud provider, a deployment target. When a governance doc's actual content
142
+ turns out to be vendor-specific throughout (found, not assumed: [[cicd-standards]] was
143
+ Azure-DevOps-specific end to end until ARC-038), the remediation is the same shape decision 2
144
+ demonstrates concretely there — split into a vendor-neutral core (principles) plus a
145
+ provider-specific profile (that vendor's mechanics), the same shape
146
+ [[development-methodology]] already uses for tracking providers. No separate "vendor-specific
147
+ standards directory" — the pattern is the split itself, applied to whichever governance doc or
148
+ spell risks the same coupling next. Enforcement: explicitly advisory prose (ARC-023) — self-graded
149
+ during spell audit; no lint scans a spell prompt or governance doc for a hard-coded CI/CD or
150
+ deployment vendor assumption.
139
151
 
140
152
  > **Maintainer-internal exemption.** A few spells operate *on the Arcane framework itself*
141
153
  > (e.g. `spell-bump`, `spell-arcane-version`) and legitimately reference repo internals like
@@ -53,13 +53,13 @@ How a web property becomes retrievable by search-engine crawlers and by the AI a
53
53
 
54
54
  ### WD-02 — Inject at the existing serving layer, not a new rendering pipeline
55
55
 
56
- **Rule: The correct injection point for per-route metadata is the layer *already* serving the built application to browsers — intercept and augment the response there (template substitution, response rewriting, an edge/proxy rule) — not a separate server-side rendering pipeline built to solve this one problem. Enforcement: explicitly advisory (ARC-023) — which layer performed the injection is indistinguishable from the outside, so no executable check or spell gate confirms it; spell-make-discoverable's Phase 2 table cites WD-02 only for Open Graph tag content, not injection-layer architecture.**
56
+ **Rule: The correct injection point for per-route metadata is the layer *already* serving the built application to browsers — intercept and augment the response there (template substitution, response rewriting, an edge/proxy rule) — not a separate server-side rendering pipeline built to solve this one problem. Enforcement: explicitly advisory (ARC-023) — which layer performed the injection is indistinguishable from the outside, so no executable check or spell gate confirms it; spell-make-discoverable's Phase 2 table no longer cites WD-02 — its Open Graph route-specificity check is unlabeled supplementary evidence, not a claim about this rule.**
57
57
 
58
58
  **Why:** That layer already sees every request and already returns the HTML document; per-route string-level substitution at that point gets the metadata into the raw response without introducing a second rendering runtime, a hydration story, or a new build target. See "Why per-route injection beats both alternatives" below for the fuller comparison.
59
59
 
60
60
  ### WD-03 — Dead or non-public routes return a real not-found status
61
61
 
62
- **Rule: A route representing content that no longer exists, or that was never public, must return a genuine "not found" HTTP status (404, or 410 if the removal is intentional and permanent) while still rendering the application's own not-found UI in the response body. Enforcement: explicitly advisory (ARC-023) — no executable check or spell gate in this codebase asserts response status codes for dead routes; spell-make-discoverable's Phase 2 table cites WD-03 for canonical-tag uniqueness, a different check.**
62
+ **Rule: A route representing content that no longer exists, or that was never public, must return a genuine "not found" HTTP status (404, or 410 if the removal is intentional and permanent) while still rendering the application's own not-found UI in the response body. Enforcement: structured spell gate (ARC-023) — spell-make-discoverable's Phase 2 Audit table (Per-route share metadata, WD-03) fetches a known-dead or non-public route and requires a genuine `404`/`410` status, not an always-`200` shell, before Phase 3's approval gate allows any fix to proceed.**
63
63
 
64
64
  **Why:** An always-200 catch-all — common with client-side routing, where every path resolves to the same shell and the app decides what to render after the fact — is invisible to a human browsing normally; they see a "not found" page and move on. A search engine reads the status code, not the rendered pixels. A 200 on a dead URL tells the crawler the page is valid and indexable — a "soft 404" — and the URL gets indexed, sometimes ranked, and eventually shown to a searcher as a dead end.
65
65
 
@@ -81,13 +81,13 @@ Between an approach that solves a narrower problem than it claims to and one tha
81
81
 
82
82
  ### WD-04 — Environment-specific values resolve at runtime
83
83
 
84
- **Rule: Any value that legitimately differs between deployed environments — most commonly the canonical public origin or hostname embedded in canonical URLs, structured data, or preview metadata — must be resolved at runtime from the actual running environment, never baked in at build time. Enforcement: explicitly advisory (ARC-023) — no executable check confirms build-time values are absent from the running artifact; spell-make-discoverable's Phase 2 table cites WD-04 for JSON-LD validity, a different check.**
84
+ **Rule: Any value that legitimately differs between deployed environments — most commonly the canonical public origin or hostname embedded in canonical URLs, structured data, or preview metadata — must be resolved at runtime from the actual running environment, never baked in at build time. Enforcement: explicitly advisory (ARC-023) — no executable check confirms build-time values are absent from the running artifact; spell-make-discoverable's Phase 2 table no longer cites WD-04 — its JSON-LD validity check is unlabeled supplementary evidence, not a claim about this rule.**
85
85
 
86
86
  **Why:** When a single build artifact is deployed unchanged to multiple environments (build once, promote the same artifact — standard practice, and good practice for other reasons), any value fixed at build time is physically identical across every environment that artifact runs in. It cannot carry an environment-specific answer no matter how carefully it was set at build time, because build time happens once, upstream of every environment the artifact will later run in. There is no way to make a single frozen value simultaneously correct for staging and production; the fix is to stop trying and read the value from the environment that's actually running.
87
87
 
88
88
  ### WD-05 — Confirm environment flags actually vary, don't trust their default
89
89
 
90
- **Rule: A generic "is this production" flag commonly defaults to the same value across *all* deployed environments, not only production. Branching discoverability logic on it (for example, suppressing indexing everywhere that isn't production) silently gives every environment the production answer unless that default has been checked. Before relying on any such signal, confirm it actually differs per environment by reading it back from each running environment — not by trusting how it was configured or assumed to behave. Enforcement: explicitly advisory (ARC-023) — confirming this requires operator judgment about which signal to trust and read back per environment; spell-make-discoverable's Phase 2 table cites WD-05 for basic robots.txt validity, a different check.**
90
+ **Rule: A generic "is this production" flag commonly defaults to the same value across *all* deployed environments, not only production. Branching discoverability logic on it (for example, suppressing indexing everywhere that isn't production) silently gives every environment the production answer unless that default has been checked. Before relying on any such signal, confirm it actually differs per environment by reading it back from each running environment — not by trusting how it was configured or assumed to behave. Enforcement: explicitly advisory (ARC-023) — confirming this requires operator judgment about which signal to trust and read back per environment; spell-make-discoverable's Phase 2 table no longer cites WD-05 — its basic robots.txt validity check is unlabeled supplementary evidence, not a claim about this rule.**
91
91
 
92
92
  **Why:** The mistake is invisible precisely because production looks correct — it's the one environment where the flag's default happens to match reality. The bug only shows up in every *other* environment, which is exactly where it's least likely to get noticed during a review that (reasonably) focuses attention on production. Prefer a signal already confirmed to vary — a runtime-resolved hostname, an explicitly-set per-environment value — over a flag whose default was never actually read back from a non-production environment.
93
93
 
@@ -97,7 +97,7 @@ Between an approach that solves a narrower problem than it claims to and one tha
97
97
 
98
98
  ### WD-06 — Never route crawlers through user-traffic side effects
99
99
 
100
- **Rule: Never route crawler or bot traffic through an endpoint that has view-counting, rate-limiting-by-request, or other side effects meant for real user traffic. Serve crawler-facing responses — sitemaps, crawler-visible pages, structured-data feeds — by reading the underlying data directly, bypassing those side-effecting code paths. Enforcement: explicitly advisory (ARC-023) — no executable check or spell gate in this codebase confirms crawler routes bypass side-effecting code paths; spell-make-discoverable's Phase 2 table cites WD-06 for sitemap-directive presence, a different check.**
100
+ **Rule: Never route crawler or bot traffic through an endpoint that has view-counting, rate-limiting-by-request, or other side effects meant for real user traffic. Serve crawler-facing responses — sitemaps, crawler-visible pages, structured-data feeds — by reading the underlying data directly, bypassing those side-effecting code paths. Enforcement: explicitly advisory (ARC-023) — no executable check or spell gate in this codebase confirms crawler routes bypass side-effecting code paths; spell-make-discoverable's Phase 2 table no longer cites WD-06 — its sitemap-directive-presence check is unlabeled supplementary evidence, not a claim about this rule.**
101
101
 
102
102
  **Why:** A well-behaved crawler generates a high volume of requests across a large route set in a short window, and that volume is not representative of user behavior even when the crawler is behaving exactly as intended. Run that volume through a view counter and the counter becomes meaningless noise on top of real signal. Run it through a rate limiter designed to catch abusive human traffic and it trips the limiter, throttling or blocking the crawler — and the content behind it — for doing nothing wrong.
103
103
 
@@ -113,7 +113,7 @@ Between an approach that solves a narrower problem than it claims to and one tha
113
113
 
114
114
  ### WD-08 — A specific allow-rule must be declared to escape a broader disallow
115
115
 
116
- **Rule: In a robots-exclusion file, when both an explicit allow-rule for a specific path (for example, a public media/asset path that page metadata references) and a broader disallow-rule for a prefix that would otherwise contain it (for example, a general API path prefix) are both relevant, the specific allow-rule *must be declared* so that it actually takes effect. Enforcement: explicitly advisory (ARC-023) — no executable check or spell gate in this codebase verifies allow/disallow specificity for a given path; spell-make-discoverable's Phase 2 table cites WD-08 for an unrelated non-production noindex-signal check, not this rule's actual claim.**
116
+ **Rule: In a robots-exclusion file, when both an explicit allow-rule for a specific path (for example, a public media/asset path that page metadata references) and a broader disallow-rule for a prefix that would otherwise contain it (for example, a general API path prefix) are both relevant, the specific allow-rule *must be declared* so that it actually takes effect. Enforcement: structured spell gate (ARC-023) — spell-make-discoverable's Phase 2 Audit table (Robots and sitemap policy, WD-08) checks that a specific `Allow:` rule is declared for any public path a broader `Disallow:` prefix would otherwise swallow, before Phase 3's approval gate allows any fix to proceed.**
117
117
 
118
118
  **Why:** Crawlers that respect a robots-exclusion file resolve overlapping rules by specificity — the most specific matching path generally wins over a broader one, not "whichever rule appears first in the file" and not "disallow always wins." Get the specificity wrong, or simply omit the specific allow-rule and assume the broader disallow won't apply to it, and a media-asset path nested under a disallowed API prefix becomes entirely invisible to image crawling — even though the page's own metadata correctly points at that image. A correct metadata reference doesn't help if the crawler was never permitted to fetch what it points to.
119
119
 
@@ -194,13 +194,13 @@ Using only the robots-exclusion rule leaves the bare-URL-listing gap open. Using
194
194
 
195
195
  ### WD-14 — Aspect ratio, safe margin, file size
196
196
 
197
- **Rule: Link-preview (share) images should follow the aspect ratio broadly adopted across major platforms — close to 1.91:1 — with a safe-content margin around the edges so platform-specific cropping doesn't cut off text or a focal subject, and should stay modest in file size. Enforcement: explicitly advisory (ARC-023) — no executable check measures aspect ratio or safe-margin compliance in this codebase; spell-make-discoverable's Phase 2 table cites WD-14 for restricted-path sitemap exclusion, a different check, and checks share-image fetchability/size under the WD-15 label instead.**
197
+ **Rule: Link-preview (share) images should follow the aspect ratio broadly adopted across major platforms — close to 1.91:1 — with a safe-content margin around the edges so platform-specific cropping doesn't cut off text or a focal subject, and should stay modest in file size. Enforcement: explicitly advisory (ARC-023) — no executable check measures aspect ratio or safe-margin compliance in this codebase; spell-make-discoverable's Phase 2 table no longer cites WD-14 — its restricted-path sitemap-exclusion check is unlabeled supplementary evidence, and share-image fetchability/size is captured without a WD-nn citation as well.**
198
198
 
199
199
  **Why:** Scrapers fetch preview images synchronously as part of unfurling a link, and some platforms silently refuse to fetch, or silently fail to display, an image above their own size ceiling — there is usually no visible error; the preview simply renders without an image. A safe-content margin matters because different platforms crop the same source image to slightly different final frames; content placed right at the edge survives on some platforms and gets cut on others.
200
200
 
201
201
  ### WD-15 — Composite real assets; don't ask a generator for exact type or logos
202
202
 
203
- **Rule: Current AI image-generation tools cannot reliably reproduce an exact existing typeface or an exact existing vector logo/mark — both come out as a visible approximation, and no amount of prompt refinement reliably fixes this. It's a property of how these generators work (they produce plausible-looking glyphs and marks probabilistically rather than reproducing exact vector paths or font outlines), not a quality issue specific to any one generation attempt. When brand-exact typography or an exact logo mark matters for a share image, composite the real font files and the real vector logo asset over separately generated artwork or background — don't ask the generator to produce the whole image including the exact type and mark. Enforcement: explicitly advisory (ARC-023) — whether generated artwork faithfully reproduces exact type or a logo is a design judgment call no automated check can verify; spell-make-discoverable's Phase 2 table cites WD-15 for share-image fetchability, a different, mechanically-checkable concern.**
203
+ **Rule: Current AI image-generation tools cannot reliably reproduce an exact existing typeface or an exact existing vector logo/mark — both come out as a visible approximation, and no amount of prompt refinement reliably fixes this. It's a property of how these generators work (they produce plausible-looking glyphs and marks probabilistically rather than reproducing exact vector paths or font outlines), not a quality issue specific to any one generation attempt. When brand-exact typography or an exact logo mark matters for a share image, composite the real font files and the real vector logo asset over separately generated artwork or background — don't ask the generator to produce the whole image including the exact type and mark. Enforcement: explicitly advisory (ARC-023) — whether generated artwork faithfully reproduces exact type or a logo is a design judgment call no automated check can verify; spell-make-discoverable's Phase 2 table no longer cites WD-15 — its share-image fetchability check is unlabeled supplementary evidence, not a claim about this rule.**
204
204
 
205
205
  ---
206
206
 
@@ -107,33 +107,38 @@ for each of `{NONPROD_ORIGINS}`:
107
107
 
108
108
  For each check below, capture the stated evidence and mark it
109
109
  `PASS`/`FAIL`. **Do not restate any rule's justification here — cite the ID
110
- only**; the reasoning lives in `web-discoverability-standards.md`.
110
+ only**; the reasoning lives in `web-discoverability-standards.md`. A row
111
+ marked *(supplementary)* is a useful check that doesn't correspond to any
112
+ single `WD-nn` rule — capture its evidence the same way, but don't cite a
113
+ rule ID for it.
111
114
 
112
115
  **Per-route share metadata**
113
116
 
114
117
  | Rule ID | Check | Evidence to capture |
115
118
  | --- | --- | --- |
116
119
  | `WD-01` | Fetch `{SITE_ORIGIN}`/&lt;a specific deep route, not the homepage&gt; with a plain HTTP client that does not execute JavaScript. Does the raw response body carry a `<title>` and meta description distinct from the homepage's? | The two raw response bodies, with the extracted `<title>`/description values highlighted. |
117
- | `WD-02` | In that same response, do the Open Graph / social-card tags (`og:title`, `og:description`, `og:image`) reflect the route's own content, not the homepage's or a hard-coded default? | Extracted `og:*` tag values per sampled route. |
118
- | `WD-03` | Does each fetched route carry exactly one `<link rel="canonical">`, self-referential or pointing at the one intended canonical URL, with no second conflicting canonical tag? | Canonical URL(s) found per route; flag any route with more than one. |
119
- | `WD-04` | Where the route type is expected to carry structured data, is an `application/ld+json` block present and does it parse as valid JSON? | The raw JSON-LD block per route, and the parse result. |
120
+ | *(supplementary)* | In that same response, do the Open Graph / social-card tags (`og:title`, `og:description`, `og:image`) reflect the route's own content, not the homepage's or a hard-coded default? | Extracted `og:*` tag values per sampled route. |
121
+ | `WD-03` | Fetch a route representing content that no longer exists, or that was never public. Does the response return a genuine `404` or `410` status — not a `200` that merely renders a not-found-looking page? | The response status code, and the rendered body (to confirm it's the application's own not-found UI, not a generic host error page). |
122
+ | *(supplementary)* | Does each fetched route carry exactly one `<link rel="canonical">`, self-referential or pointing at the one intended canonical URL, with no second conflicting canonical tag? | Canonical URL(s) found per route; flag any route with more than one. |
123
+ | *(supplementary)* | Where the route type is expected to carry structured data, is an `application/ld+json` block present and does it parse as valid JSON? | The raw JSON-LD block per route, and the parse result. |
120
124
  | `WD-13` | Where a share-metadata or structured-data value is built from user- or content-supplied text (e.g. a title containing a quote or ampersand), is it correctly escaped for both contexts it appears in — JSON-escaped inside the JSON-LD payload, and separately HTML-escaped in surrounding markup — with no raw pass-through? | One sampled route whose content contains a quote, ampersand, or angle bracket, with the rendered JSON-LD and HTML attribute shown side by side. |
121
- | `WD-15` | Fetch a sampled `{PUBLIC_ASSET_PREFIX}` share-image URL with a plain, unauthenticated HTTP client. Does it return `200` with an image content-type and no auth redirect? | Response status, `Content-Type` header, and byte size for the sampled asset. |
125
+ | *(supplementary)* | Fetch a sampled `{PUBLIC_ASSET_PREFIX}` share-image URL with a plain, unauthenticated HTTP client. Does it return `200` with an image content-type and no auth redirect? | Response status, `Content-Type` header, and byte size for the sampled asset. |
122
126
 
123
127
  **Robots and sitemap policy**
124
128
 
125
129
  | Rule ID | Check | Evidence to capture |
126
130
  | --- | --- | --- |
127
- | `WD-05` | Fetch `{SITE_ORIGIN}/robots.txt` directly. Does it exist, return `200`, parse as valid, and avoid disallowing any route intended to be indexed? | Raw `robots.txt` body, and any `Disallow` line matching an intended-public route. |
128
- | `WD-06` | Does `robots.txt` contain a `Sitemap:` directive, and does fetching that URL return a well-formed sitemap? | The `Sitemap:` line and the fetched sitemap's parse result. |
131
+ | *(supplementary)* | Fetch `{SITE_ORIGIN}/robots.txt` directly. Does it exist, return `200`, parse as valid, and avoid disallowing any route intended to be indexed? | Raw `robots.txt` body, and any `Disallow` line matching an intended-public route. |
132
+ | *(supplementary)* | Does `robots.txt` contain a `Sitemap:` directive, and does fetching that URL return a well-formed sitemap? | The `Sitemap:` line and the fetched sitemap's parse result. |
129
133
  | `WD-07` | Does every URL listed in the sitemap satisfy `{PUBLIC_VISIBILITY_PREDICATE}`? | Any sitemap entries that fail the predicate. |
130
- | `WD-14` | For a sampled route under `{RESTRICTED_PREFIX}`: is it absent from the sitemap, does it carry a noindex signal, and is it unreachable via any link from a route that satisfies `{PUBLIC_VISIBILITY_PREDICATE}`? | Sitemap membership (should be none), noindex signal, and an inbound-link check from the public route set. |
134
+ | `WD-08` | Where `{PUBLIC_ASSET_PREFIX}` (or another specific public path referenced by page metadata) falls under a broader disallowed prefix in `robots.txt`, is an explicit `Allow:` rule declared for that specific path? | The relevant `Allow:`/`Disallow:` lines from `robots.txt`, and confirmation the specific path is fetchable by a crawler despite the broader disallow. |
135
+ | *(supplementary)* | For a sampled route under `{RESTRICTED_PREFIX}`: is it absent from the sitemap, does it carry a noindex signal, and is it unreachable via any link from a route that satisfies `{PUBLIC_VISIBILITY_PREDICATE}`? | Sitemap membership (should be none), noindex signal, and an inbound-link check from the public route set. |
131
136
 
132
137
  **Non-production noindexing**
133
138
 
134
139
  | Rule ID | Check | Evidence to capture |
135
140
  | --- | --- | --- |
136
- | `WD-08` | For each `{NONPROD_ORIGINS}` host: is a site-wide `Disallow: /` in `robots.txt` the *only* suppression signal present — no accompanying noindex header or meta tag? | The host's `robots.txt` plus presence/absence of a noindex header and meta tag on a sampled route. |
141
+ | *(supplementary)* | For each `{NONPROD_ORIGINS}` host: is a site-wide `Disallow: /` in `robots.txt` the *only* suppression signal present — no accompanying noindex header or meta tag? Closely related to `WD-09` below. | The host's `robots.txt` plus presence/absence of a noindex header and meta tag on a sampled route. |
137
142
  | `WD-09` | For each `{NONPROD_ORIGINS}` host: does a sampled route serve **both** an `X-Robots-Tag: noindex` response header **and** an HTML `<meta name="robots" content="noindex">` tag, gated by `{ENV_SIGNAL}`? | Response headers and parsed `<head>` for one sampled route per non-prod host. |
138
143
 
139
144
  **Search-engine registration**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arcane-cli",
3
- "version": "0.32.0",
3
+ "version": "0.32.3",
4
4
  "description": "Arcane framework CLI — scaffold and manage governance files in consuming repositories",
5
5
  "keywords": [
6
6
  "arcane",