@sleepy_panda_srl/portulan 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +5 -0
  3. package/README.md +87 -0
  4. package/cli/README.md +337 -0
  5. package/cli/compile.mjs +2209 -0
  6. package/cli/control-chars.mjs +570 -0
  7. package/cli/discover.mjs +985 -0
  8. package/cli/doctor.mjs +2803 -0
  9. package/cli/feedback.mjs +970 -0
  10. package/cli/gate.mjs +271 -0
  11. package/cli/index.mjs +1390 -0
  12. package/cli/init.mjs +1729 -0
  13. package/cli/librarian.mjs +1384 -0
  14. package/cli/manifest.mjs +59 -0
  15. package/cli/new.mjs +803 -0
  16. package/cli/pack-version.mjs +543 -0
  17. package/cli/plugin-lint.mjs +1566 -0
  18. package/cli/portulan.mjs +266 -0
  19. package/cli/recipe-set.mjs +425 -0
  20. package/cli/rule-carriers.mjs +489 -0
  21. package/cli/skills-set.mjs +715 -0
  22. package/cli/stop-gate.mjs +434 -0
  23. package/cli/upgrade.mjs +897 -0
  24. package/cli/vendor.mjs +1504 -0
  25. package/core/README.md +46 -0
  26. package/core/engine.md +47 -0
  27. package/core/operating/autonomy.md +100 -0
  28. package/core/operating/evolution.md +84 -0
  29. package/core/operating/loop.md +83 -0
  30. package/core/operating/memory.md +161 -0
  31. package/core/operating/safety.md +42 -0
  32. package/core/operating/verification.md +76 -0
  33. package/core/personas/README.md +40 -0
  34. package/core/personas/implementer.md +51 -0
  35. package/core/personas/librarian.md +73 -0
  36. package/core/personas/reviewer.md +52 -0
  37. package/core/skills/README.md +52 -0
  38. package/core/skills/clarify/SKILL.md +42 -0
  39. package/core/skills/codify/SKILL.md +65 -0
  40. package/core/skills/consolidate/SKILL.md +121 -0
  41. package/core/templates/gate-policy.md +76 -0
  42. package/core/templates/handoff.md +29 -0
  43. package/core/templates/memory-entry.md +44 -0
  44. package/core/templates/pack.md +94 -0
  45. package/core/templates/persona.md +69 -0
  46. package/core/templates/proposal.md +27 -0
  47. package/core/templates/repo-card.md +28 -0
  48. package/core/templates/skill.md +57 -0
  49. package/core/templates/task.md +26 -0
  50. package/core/templates/workspace.md +81 -0
  51. package/package.json +41 -0
  52. package/packs/.claude-plugin/plugin.json +22 -0
  53. package/packs/README.md +61 -0
  54. package/packs/rituals/README.md +13 -0
  55. package/packs/rituals/checkpoints/README.md +105 -0
  56. package/packs/rituals/checkpoints/pack.json +36 -0
  57. package/packs/rituals/checkpoints/personas/supervisor.md +64 -0
  58. package/packs/rituals/checkpoints/skills/milestone-close/SKILL.md +75 -0
  59. package/packs/rituals/checkpoints/skills/pre-commit/SKILL.md +75 -0
  60. package/packs/rituals/checkpoints/skills/session-open/SKILL.md +57 -0
  61. package/packs/stacks/README.md +8 -0
  62. package/packs/tools/README.md +17 -0
  63. package/packs/tools/github/README.md +65 -0
  64. package/packs/tools/github/pack.json +29 -0
  65. package/packs/tools/github/verify/README.md +51 -0
  66. package/packs/tools/github/verify/actions-pinned.sh +132 -0
  67. package/spec/README.md +399 -0
  68. package/spec/migrations/0001-repository-declares-its-tree.mjs +100 -0
  69. package/spec/migrations/0002-bundle-fallback-path.mjs +179 -0
  70. package/spec/migrations/README.md +95 -0
  71. package/spec/pack.schema.json +201 -0
  72. package/spec/slots.md +796 -0
  73. package/spec/workspace.schema.json +438 -0
package/spec/slots.md ADDED
@@ -0,0 +1,796 @@
1
+ # The slots, and where each came from
2
+
3
+ > Companion to [`workspace.schema.json`](workspace.schema.json). One section per slot: what it holds,
4
+ > **why it is a slot at all**, what it was derived from, and what [`doctor`](../cli/doctor.mjs) checks
5
+ > about it. Every "what `doctor` checks" line below was a specification of intent when this file was
6
+ > written and is a description of behaviour now — the second milestone-2 session built the validator,
7
+ > and where it does **less** than a line here promised, the line says so rather than being quietly
8
+ > softened.
9
+ >
10
+ > The derivation column is not decoration. A schema is the easiest place in a product to smuggle in
11
+ > invention — a slot added for symmetry costs every future adopter a decision they did not need to make.
12
+ > Every slot below traces to at least one of three sources, and a slot that traced to none was cut:
13
+ >
14
+ > - **criterion** — the milestone-2 exit criterion in [`../docs/plan.md`](../docs/plan.md), which names
15
+ > the slots this milestone owes.
16
+ > - **constitution** — [`../docs/vision.md`](../docs/vision.md): thesis 1's definition of a workspace, or
17
+ > another clause named inline. The parenthetical is deliberately **not** reproduced here. A quotation of
18
+ > the constitution inside a document about accurate derivation is a drift surface, and this one had
19
+ > already drifted: an earlier draft appended the milestone-2 criterion's *"plus the constitution and
20
+ > product-layer slots"* to thesis 1's own list, which does not contain that phrase. Follow the link.
21
+ > - **content** — the one real workspace that existed when this was written,
22
+ > [`../.portulan/`](../.portulan/). The plan sequences re-expression before schema on purpose: derive
23
+ > the spec from real material rather than guessing it.
24
+ >
25
+ > Fragments below use `{braces}` for placeholders, as [`../core/templates/`](../core/templates/) does.
26
+ > Nothing here embeds customer zero's prose — it is cited by link, because a spec that quoted the first
27
+ > workspace would outlive it badly.
28
+
29
+ ## The minimality rule
30
+
31
+ A slot is **required** only if a workspace lacking it breaks a promise the engine makes. Everything else
32
+ is optional, however good an idea it is. The pressure runs the other way — a schema author's instinct is
33
+ that a workspace *should* have memory, handoffs, and proposals — but a team adopting Portulan on a
34
+ Tuesday afternoon has none of those yet, and a definition that rejects them has made the first
35
+ experience a failure. *(Binding non-goal: no ceremony that can't scale down.)*
36
+
37
+ ## Quick reference
38
+
39
+ | Slot | Carrier | Required | Derived from |
40
+ |---|---|---|---|
41
+ | `portulan.spec` | manifest | **yes** | constitution — the plan's Versioning protocol; migrations need a version to compare |
42
+ | `name` | manifest | **yes** | constitution — workspaces ship as plugins through a feed, so they need a stable identifier |
43
+ | `kind` | manifest | **yes** | content — [`three-workspaces-not-one.md`](../.portulan/memory/three-workspaces-not-one.md); constitution — *Reference workspaces* |
44
+ | `summary` | manifest | no | constitution — attention budgets per layer |
45
+ | `slots.identity` | path | **for a governing kind** | constitution — thesis 1 *(identity, stack, glossary)*; content — [`identity.md`](../.portulan/identity.md) |
46
+ | `slots.principles` | path | **for a governing kind** | criterion — *constitution … slots (team principles)*; content — [`principles.md`](../.portulan/principles.md) |
47
+ | `slots.constitution` | path | no | content — customer zero is graded against a document outside its workspace; constitution — spec-driven school, the constitution file |
48
+ | `slots.gates` | path | **for a governing kind** | criterion — *gate map*; constitution — [`autonomy.md`](../core/operating/autonomy.md) puts the map in the workspace |
49
+ | `slots.dod` | path | no | constitution — thesis 1 *(DoD)*; [`verification.md`](../core/operating/verification.md) — core supplies the floor |
50
+ | `slots.memory` | path | no | constitution — thesis 1 *(memory)*; [`memory.md`](../core/operating/memory.md) |
51
+ | `slots.repos` | path | no | constitution — thesis 1 *(repo cards)*; [`repo-card.md`](../core/templates/repo-card.md) |
52
+ | `slots.tasks` | path | no | content — [`tasks/`](../.portulan/tasks/); constitution — BMAD, the story file as atomic context unit |
53
+ | `slots.handoffs` | path | no | content — [`handoffs/`](../.portulan/handoffs/); [`loop.md`](../core/operating/loop.md) — every session ends with one |
54
+ | `slots.proposals` | path | no | content — [`proposals/`](../.portulan/proposals/); [`evolution.md`](../core/operating/evolution.md) |
55
+ | `verify` | structured | **for a governing kind** | criterion — *verify recipes*; [`verification.md`](../core/operating/verification.md) — the workspace sets the default |
56
+ | `governed_by` | manifest | **for `kind: pointer`** | the maintainer's residence ruling, 2026-07-30 — [proposal 0017](../.portulan/proposals/0017-one-repository-one-governing-workspace.md); one repository, one governing workspace |
57
+ | `products[]` | structured | no | criterion — *product-layer slot … portfolio-aware*; content — [`identity.md`](../.portulan/identity.md) |
58
+ | `affordances` | path | no | criterion — *agent-affordances slot*; constitution — the agent-native / AX row of the influence map |
59
+ | `tree` | path | **for `kind: repository`** | criterion — *lints workspace claims against the tree*; content — [`../examples/`](../examples/), the first workspace whose repositories are not present |
60
+ | `packs` | structured | no | constitution — thesis 1's cascade, `core < pack < workspace` |
61
+ | `memory` | structured | no | criterion — milestone 5, *generated size-budgeted index whose budget is a rail*; [`memory.md`](../core/operating/memory.md) — the Index and Consolidate states of the lifecycle |
62
+ | `handoffs` | structured | no | criterion — milestone 5 as amended, *a generated index over the handoff series*; [`loop.md`](../core/operating/loop.md) — the librarian that mines the series |
63
+ | `provenance` | record field | **on every rule** | criterion — *provenance slot*; [proposal 0002](../.portulan/proposals/0002-sealed-provenance.md), adopted |
64
+
65
+ ## `kind` — which of the four workspace kinds this is, and which of them govern
66
+
67
+ The strongest single piece of evidence in the tree asked for this slot by name. The memory entry
68
+ [`three-workspaces-not-one.md`](../.portulan/memory/three-workspaces-not-one.md) records its own
69
+ retirement condition as *"the Workspace Definition (milestone 2) names and distinguishes the three
70
+ formally"* — so the schema is not inventing a distinction, it is discharging one the workspace had
71
+ already written down and was waiting on.
72
+
73
+ `repository` · `demo` · `portfolio` — the three that **govern** — and, added at 2.7, `pointer`, which
74
+ does not. **What `doctor` checks:** that the value is one of the four, and then which form the manifest
75
+ must take (below). What it cannot check is the thing that matters about the first three — whether a
76
+ `demo` workspace actually carries no real internal policy. That stays human review, and it is the reason
77
+ the slot is required rather than inferred from a path: an author who has to type `demo` has been asked
78
+ the question.
79
+
80
+ ### `pointer` — a repository whose workspace resides elsewhere
81
+
82
+ Added at **2.7**, from the maintainer's residence ruling of 2026-07-30
83
+ ([proposal 0017](../.portulan/proposals/0017-one-repository-one-governing-workspace.md)):
84
+
85
+ > A repository is governed by exactly one workspace. It carries its own full workspace, or a pointer to
86
+ > the workspace that names it — never both.
87
+
88
+ A team's workspace can reside in two places, and the choice is the customer's: **in the repository**, the
89
+ shape customer zero has, self-contained and feed-independent; or **feed-side**, in a portfolio workspace
90
+ that names the repository, which reaches many repositories at once and keeps team context out of product
91
+ trees whose audiences may be wider. They are one artifact in two residences. What differs is reach and
92
+ delivery; what does not differ is what the workspace contains, or what a customer can do with it.
93
+
94
+ Which is exactly why both may not be present at once. Two residences of the same thing are not two
95
+ things — they are one thing written down twice, with nothing holding the copies in agreement. `pointer`
96
+ is what makes the second residence expressible without a second copy: a manifest that names its governor
97
+ and carries no policy layer of its own.
98
+
99
+ **Why a `kind` value rather than a separate file.** The boot skill looks for exactly one path,
100
+ `${CLAUDE_PROJECT_DIR}/.portulan/workspace.json`, and does not search outward. A pointer that lived
101
+ anywhere else would be invisible to the one thing that most needs to read it, and a repository governed
102
+ from a feed would be indistinguishable from a repository that never adopted Portulan.
103
+
104
+ **What `doctor` checks:** that a pointer carries `governed_by`, and that it carries no *governing* keys —
105
+ no slots, no recipes, no products, no packs, no store. The permit-list is exactly `portulan`, `name`,
106
+ `kind`, `governed_by` and an optional `summary`; `summary` is on it because one line an agent reads before
107
+ loading anything else costs nothing and is the difference between "governed elsewhere" and a bare name.
108
+ The schema requires `governed_by` through its `oneOf`, and `doctor` refuses anything outside the
109
+ permit-list in the ruling's own words. It
110
+ also skips every governing-workspace check for a pointer **and says so**: a pointer declares no recipes,
111
+ so `verify.default` alone would fail every compliant one, and a check that vanishes without a word is
112
+ worse than one that admits what it could not reach.
113
+
114
+ **What `doctor` resolves and does not grade, since milestone 7:** `governed_by` is dereferenced against
115
+ the host's installed-plugin record by [`../cli/discover.mjs`](../cli/discover.mjs) — on disk, never over
116
+ the network — and the answer is **reported**. Where the governing workspace is installed, the report
117
+ names the directory and what to run against it; where it is not, the honest *not installed here*
118
+ sentence; where two installs answer to one name, both are named and neither is chosen. **No outcome
119
+ moves the verdict**: a pointer whose governor is uninstalled is a correct pointer, and a tool whose exit
120
+ code turned on somebody's install state would be judging a host rather than a workspace.
121
+
122
+ **What nothing checks:** that the workspace a pointer names *governs*, or is green. Being resolvable
123
+ says where it is, and running `doctor` against that directory is the separate act that grades it — see
124
+ [`README.md`](README.md)'s coverage table.
125
+
126
+ ## `slots.identity` and `slots.principles` — and why they are two
127
+
128
+ Thesis 1 lists identity, stack, and glossary as one cluster; the criterion separately names a
129
+ constitution slot glossed as *team principles*. Customer zero had all of it in one document, and the
130
+ split was forced by the no-fragments rule rather than chosen: two slots cannot address two halves of one
131
+ file when a slot must be a whole file.
132
+
133
+ The split turned out to be the right cut anyway. **Identity** answers *who we are and what we work with*
134
+ — team shape, stack, glossary. **Principles** answers *how we decide*, and it is the half other work is
135
+ graded against. They change on different clocks: a stack row turns over every year or two, a principle
136
+ almost never.
137
+
138
+ **What `doctor` checks:** both resolve to files that exist — files specifically, not directories, since
139
+ both are declared `filePath`.
140
+
141
+ ## `slots.constitution` — optional, and the one slot expected to escape
142
+
143
+ The criterion glosses the constitution slot as team principles, which is right for most adopters: their
144
+ principles *are* the standard they are graded against. Customer zero is the case that shows those can be
145
+ two different things — its principles are workspace policy, while
146
+ [`../docs/vision.md`](../docs/vision.md) is the product's constitution, human-owned, sitting outside the
147
+ workspace directory entirely.
148
+
149
+ So the schema takes both: `principles` is required and lives in the workspace; `constitution` is optional
150
+ and may point anywhere. Had containment been required, `doctor`'s first run against its own author's
151
+ workspace would have gone red for a schema-design reason rather than a content one — a good early
152
+ warning, arriving after the design was frozen.
153
+
154
+ **What `doctor` checks:** the path resolves. It also **reports — never fails —** when *any* path slot's
155
+ target escapes the workspace directory. The distinction matters for whoever implements it: the schema
156
+ permits escaping everywhere, because a `pattern` sees a string and not the key it belongs to, so "only
157
+ `constitution` may escape" is a convention this document states and `doctor` surfaces, not a constraint
158
+ the schema carries. Reporting is the right severity — a workspace embedded in a larger repository may
159
+ legitimately reach for a shared document, and failing would make the tool wrong about a case it cannot
160
+ see.
161
+
162
+ One consequence worth naming before `doctor` is written: this slot takes `$defs/path`, which admits a
163
+ directory as well as a file, so `"constitution": "docs/"` validates. That is deliberate — a team's
164
+ constitution may genuinely be a folder rather than one document — and it means `doctor` resolves the
165
+ target without assuming it is a file. Same severity as an escape: report what it found, do not fail.
166
+
167
+ ## `slots.gates` — the policy half of autonomy
168
+
169
+ [`autonomy.md`](../core/operating/autonomy.md) defines the tiers as universal mechanism and says
170
+ explicitly that which concrete action lands in which tier is workspace policy. That makes the gate map
171
+ the one slot where core has already promised the workspace will answer, so a workspace without it leaves
172
+ an engine promise unfulfilled — required, by the minimality rule's own test.
173
+
174
+ **One convention this slot carries, and it is load-bearing.** `doctor` finds the required-check claim by
175
+ looking for a Markdown table row whose first cell matches **`required status check`** (case-insensitive)
176
+ and reading the backticked values from the rest of the row. That is the only structure the tool expects
177
+ of a gate map — everything else in the file is prose it never parses. It is written here because nothing
178
+ else states it: there is no gate-map template in [`../core/templates/`](../core/templates/), so an adopter
179
+ whose floor table uses a different row label gets the check **skipped** — no comparison happens at all.
180
+ It is no longer *silent*: `doctor` reports that the gate map names no required status check, and names
181
+ both readings, so a reader cannot mistake *this workspace requires none* for *I did not recognise your
182
+ table*. (It was silent — nothing reported, nothing counted — until a review measured it.) Naming the
183
+ convention is the cheap fix; a template is the better one and is not built.
184
+
185
+ **What `doctor` checks:** the path resolves; and, where the workspace declares a `tree`, that **every**
186
+ status check the gate map says `main` requires is one a workflow in that tree actually reports. That is
187
+ the gate map's half of the claims-against-the-tree lint, and it is narrower than "does not claim
188
+ enforcement the repository does not have": it compares named contexts against what the workflows declare.
189
+
190
+ Two details cost a false green each before a third workspace exposed them, and both are worth knowing
191
+ before writing a gate map. **A job's reported context is its `name:` when it has one, and its id
192
+ otherwise** — so a gate map naming the id of a job that carries a display name names something no check
193
+ will ever report, and `doctor` says exactly that rather than "no such job". And **a row may name several
194
+ checks**; reading only the first silently exempts the rest. Neither could surface on customer zero, whose
195
+ workflow deliberately sets no `name:` so that id and context coincide, and which requires exactly one
196
+ check.
197
+
198
+ Whether branch protection *really* requires those contexts, and which app they are pinned to, are live
199
+ settings — and a network call is not something a verify recipe here is allowed to make. Worth knowing
200
+ when checking by hand: a branch protected by a **repository ruleset** returns `404 Branch not protected`
201
+ from the classic `…/branches/main/protection` endpoint, which is a check reporting *not clear* about
202
+ something that is fine.
203
+
204
+ ## `gates` — the policy, as distinct from the argument for it
205
+
206
+ **Added in 2.1, optional.** A path to a JSON file the enforcement compiler reads. It is a *second* key
207
+ beside `slots.gates`, and the duplication is the point rather than an oversight: they answer different
208
+ questions and only one of them compiles.
209
+
210
+ - **`slots.gates`** → Markdown. *Why* an action sits in a tier. Read by humans; no tool dispatches on it.
211
+ - **`gates`** → JSON. *Which* action sits in which tier, in a vocabulary a backend can translate.
212
+
213
+ The alternative shapes were both considered and both refused. **Generating the prose from the JSON** adds
214
+ a build step to a product whose thesis is that the files *are* the product. **Extracting the JSON from the
215
+ prose** needs the ambitious parser this document already warns against elsewhere — it would produce false
216
+ reds, and a false red is what gets a whole check switched off.
217
+
218
+ So two files state one policy, which is this repository's signature defect, and it is contained rather than
219
+ denied: every rule carries an `id`, the prose cites those ids in code spans, and **membership is checked
220
+ both ways** — a rule with no mention, or a mention with no rule, fails. The forward direction is exact. The
221
+ reverse recognises a citation by *shape*, so it is a strong check rather than a total one, and the workspace
222
+ that implements it says so at the point of use. What no check can hold is whether a
223
+ *sentence* contradicts the rule it names. Hence the tie-break, stated in both files: **where they disagree,
224
+ the policy wins**, because it is the one that compiles.
225
+
226
+ ### The action vocabulary is the workspace's, never a host's
227
+
228
+ A rule says `{"shell": "git push"}`, not `Bash(git push:*)`. That is what keeps "LLM-agnostic by
229
+ construction" true at this layer: a second backend translates the same policy instead of forcing every
230
+ adopter to rewrite theirs. It is cheapest to hold while exactly one backend exists, which is why it was
231
+ held then.
232
+
233
+ ### Four tier classes, where core names three
234
+
235
+ `auto` · `propose` · `gated` · **`prohibited`**. The fourth is not a stronger `gated` — it is a different
236
+ answer. Gated means *approvable per action* and compiles to a prompt; prohibited means *no approval
237
+ exists* and compiles to a refusal. A three-class policy would file "no agent edits the constitution" under
238
+ Gated, and the compiler would then emit a prompt — turning *never* into *unless someone clicks yes*.
239
+
240
+ ### What a compiler must do with it
241
+
242
+ Two obligations, both learned from defects rather than designed in the abstract:
243
+
244
+ 1. **Account for every rule.** Each ends as *compiled* or *refused with a stated reason*, and the two
245
+ counts sum to the input. A rule that goes in and produces nothing leaves a policy that reads as
246
+ enforced and a machine that enforces nothing.
247
+ 2. **Refuse what it cannot compile.** An unknown tier or action shape fails the *whole* compile rather
248
+ than dropping one rule — skipping and enforcing are indistinguishable from outside
249
+ ([`../.portulan/memory/a-checker-must-refuse-what-it-cannot-check.md`](../.portulan/memory/a-checker-must-refuse-what-it-cannot-check.md)).
250
+
251
+ ### `floor` — the platform floor this policy compiles to
252
+
253
+ **Added in 2.2, optional**, inside the gate policy rather than the manifest — it is policy, and policy
254
+ lives where the rules live. Four keys, and each one exists because it varies per repository and the
255
+ floor backend would otherwise have to guess it:
256
+
257
+ | Key | What it declares |
258
+ |---|---|
259
+ | `branch` | The ref the floor protects. No default: a compiler that invents the ref it gates has stopped compiling policy and started writing it. |
260
+ | `checks` | The status checks that must be green, each `{ context, integration_id? }`. An unpinned context is satisfiable by *any* app reporting that name — permitted, and reported by `doctor`. |
261
+ | `reviews` | Required approving reviews. Declared rather than defaulted: 0 and 1 are a real difference, and a repository with one maintainer cannot use 1, because GitHub does not permit approving one's own pull request. |
262
+ | `resolve_conversations` | Whether review threads must be resolved before merge. Omitting it would export a floor weaker than the one many repositories already run. |
263
+
264
+ **What is deliberately NOT declarable is `strict`.** A pull request may not merge from behind its base
265
+ ([proposal 0011](../.portulan/proposals/0011-no-merge-from-behind-main.md), applied live on
266
+ 2026-07-27), so the export forces strict required status checks unconditionally. A policy able to
267
+ declare `strict: false` would be a compiled artifact quietly undoing a ruling, in a diff nobody would
268
+ read as one.
269
+
270
+ A workspace with no `floor` compiles no floor: the backend refuses every rule *by name*, says the key
271
+ is missing, and writes nothing. That is a legitimate shape — a workspace whose repositories are not on
272
+ GitHub, or whose floor is configured by hand, or which has not got there yet. It is **not** the demo
273
+ workspace's, which declares no gate policy at all and so never reaches this backend; saying otherwise
274
+ would credit a fixture with exercising a path it does not.
275
+
276
+ **What `doctor` checks:** that the path resolves; that every `floor.checks` context is reported by a
277
+ workflow job in the declared tree (a **failure** — a required context that never reports blocks every
278
+ pull request, and `enforce_admins` leaves nobody able to force past it); and, as a report, how much of
279
+ the policy each backend compiles, which gates no backend compiles at all, and whether the `floor` and
280
+ the gate map's required-check row agree. It does **not** compile the artifact or check the prose
281
+ citations — that lives in the compiler's suite, anchored to the real tree, because the citation
282
+ convention is this repository's and not the spec's. A workspace may declare prose and no policy; that
283
+ is an ordinary state, and it is the demo workspace's state.
284
+
285
+ ## `verify` — the only slot that is structured because it is *consumed*
286
+
287
+ Every other content slot points at Markdown for a human. This one carries data because the Stop-gate
288
+ needs an answer, not a paragraph: which recipe runs when nothing more specific applies. A prose sentence
289
+ saying "run the docs recipe" cannot be dispatched.
290
+
291
+ **As of milestone 2 that argument has a consumer rather than a promise behind it.** This repository's CI runs
292
+ the set the manifest **yields** — every recipe it declares, plus the ones its composed packs contribute
293
+ since milestone 7's composition amendment — computed by
294
+ [`../cli/recipe-set.mjs`](../cli/recipe-set.mjs) rather than enumerated in the workflow. So declaring a
295
+ recipe is what enforces it — no workflow edit, no branch-protection change, and no window in which a new
296
+ recipe exists but nothing runs it. It is a milestone early (the Stop-gate runner is milestone 4) and it is the cheapest possible
297
+ demonstration that a structured slot earns being structured. Recorded in customer zero's
298
+ [proposal 0004](../.portulan/proposals/0004-ci-runs-every-declared-recipe.md).
299
+
300
+ ```json
301
+ "verify": {
302
+ "default": "{recipe-id}",
303
+ "recipes": [
304
+ { "id": "{recipe-id}", "run": "{command}", "requires": ["{tool}"], "doc": "{path}" }
305
+ ]
306
+ }
307
+ ```
308
+
309
+ `requires` exists because of an exit code. The recipes distinguish *red* (exit 1, the check ran and
310
+ failed) from *could not run* (exit 2, a tool is missing), and that distinction is worthless if nothing
311
+ declares what was needed. Customer zero now has two recipes with different needs — one POSIX-only, one
312
+ requiring `node` — which is exactly the case that made the field earn its place.
313
+
314
+ **What `doctor` checks:** `default` names a recipe that exists in `recipes`; each `doc` **that is
315
+ present** resolves. `doc` is optional by the minimality rule — a recipe whose limits are not yet written
316
+ up is still a runnable recipe, and requiring the write-up would be exactly the ceremony that rule refuses.
317
+ It does **not** run the recipes, and it does not check that `requires` is honest — a recipe that quietly needs a
318
+ tool it did not declare passes. That gap narrowed in milestone 4 rather than closing: the Stop-gate runner
319
+ executes the **default** recipe, so that one recipe's `requires` is now tested by being run. Every other
320
+ declared recipe is still read and never executed, and `doctor` remains the wrong place to fix it — a
321
+ validator that ran arbitrary commands out of a manifest would be a validator you could not safely point
322
+ at someone else's workspace.
323
+
324
+ ## `products[]` — repeated, even though customer zero has one
325
+
326
+ The criterion says *portfolio-aware: many products per workspace*, and the vision's Reference workspaces
327
+ bullet describes a Sleepy Panda SRL workspace covering **all** Sleepy Panda SRL products. So the product layer is
328
+ per-product from the start, not a workspace-level field that a later milestone has to unpick.
329
+
330
+ This is the schema's most deliberate act of resisting its own evidence. Derived faithfully from the only
331
+ available sample, `products` would be a singular object — customer zero has exactly one product, and a
332
+ one-sample derivation would have modelled it that way, correctly for today and wrongly for milestone 6.
333
+
334
+ ```json
335
+ "products": [
336
+ { "id": "{id}", "name": "{Display Name}",
337
+ "product": "{path}", "affordances": "{path}", "repos": ["{repo-card-name}"] }
338
+ ]
339
+ ```
340
+
341
+ `products` is optional: a team adopting Portulan for one repository need not name a product at all, and
342
+ the engine still resolves everything else. Note the distinction that keeps this honest — the *definition*
343
+ has a product-layer slot, which is what the criterion requires; *instances* may leave it empty.
344
+
345
+ **What `doctor` checks:** each `product` path resolves; each `affordances` path **that is present**
346
+ resolves; each `repos` entry names a card in the `repos` slot. Per-product `affordances` is optional
347
+ because a product may inherit the workspace-level default — so the check is conditional, and `doctor`
348
+ *reports* a product that has neither its own nor an inherited one rather than failing it. (An earlier
349
+ draft of this line said `doctor` checks `affordances` unconditionally, which would have made the spec
350
+ demand something the schema declares optional; caught in review of the change that introduced it.) The
351
+ claim it cannot check is whether the mission written there is still true.
352
+
353
+ ## `affordances` — what the product offers an agent
354
+
355
+ The unclaimed niche in the influence map: *agent-legibility, repo affordances scored by `doctor`*. The
356
+ slot holds what an agent can rely on — entry points, contracts, budgets, commands — **and** what it must
357
+ not assume, which is the half that makes the document worth reading. Customer zero's
358
+ [affordances document](../.portulan/products/portulan/affordances.md) is a filled example.
359
+
360
+ It resolves down the cascade *inside* the workspace: a workspace-level `affordances` is the default, and
361
+ a product may override it. That is thesis 1's more-specific-wins rule applied one level lower, and it
362
+ exists because a portfolio workspace covering a Markdown framework and a deployed service cannot offer
363
+ one honest answer for both.
364
+
365
+ **What `doctor` checks:** the path resolves — **and, since milestone 7 session 7, it opens the document
366
+ and scores it.** The agent-legibility score named in the vision's delivery tiers is built: **seven**
367
+ dimensions, of which two read this slot — that every product has a document reachable here (its own, or
368
+ the workspace-level default above), and that each such document names what an agent must **not** assume.
369
+ The other five read the manifest, because a score confined to this slot could not discriminate between
370
+ two workspaces at all; **this slot is the score's named input rather than its only one**. Two limits
371
+ worth having in front of you: the score **moves no exit code**, since a measurement that could fail a
372
+ workspace would make conformance a function of how much prose somebody wrote; and the *must not assume*
373
+ dimension is a **form** check over a named table of headings, so a section with an empty body passes it.
374
+ The claim it still cannot check is whether what the document promises is true. One memory entry
375
+ ([`readme-map-must-match-shape.md`](../.portulan/memory/readme-map-must-match-shape.md)) records its own
376
+ retirement as conditional on that score superseding the README map — worth stating plainly that this
377
+ milestone does **not** fire that condition.
378
+
379
+ ## `tree` — where the claims are checked against, and why it is declared rather than inferred
380
+
381
+ Added in v2.0, slot and requirement together (an optional-slot draft never reached `main`), and it
382
+ exists because the claims lint needed an answer to a question nobody had asked while there was only one
383
+ workspace: *which* tree does a repo card's `test:` line refer to?
384
+
385
+ Customer zero's answer is obvious — the repository the workspace sits in, one level up. The demo's
386
+ answer is that there isn't one: Rooftop's repositories are fictional, so its cards' build/test/run lines
387
+ and layout point at directories no tree contains. Both must validate, and a lint that failed the second
388
+ would be wrong about it rather than right.
389
+
390
+ The tempting inference is `kind`: check for `repository`, skip for `demo`. It was rejected for two
391
+ reasons, and the second is the stronger one.
392
+
393
+ - **It breaks on its own second case.** A `portfolio` workspace covers many repositories, none of which
394
+ is the one it sits in. It is not a demo, and it has no single tree either — so kind-dispatch would
395
+ demand a lint it cannot run, one milestone after v1.
396
+ - **It disables a check class on a self-declared field.** `kind` is typed by an author, and `doctor`
397
+ checks only that it is one of the permitted values. A whole category of checking that switches itself off
398
+ because somebody wrote `demo` is a fail-open with a doorbell on it — this repository has now recorded
399
+ three of that shape ([`../.portulan/memory/verify-preconditions-fail-closed.md`](../.portulan/memory/verify-preconditions-fail-closed.md)),
400
+ every one of them in gate machinery.
401
+
402
+ So the workspace declares it. Present means *lint these claims against that tree*; absent means *these
403
+ claims describe something not present here*.
404
+
405
+ **And a `repository` workspace must declare it** — spec 2.0, from
406
+ [proposal 0005](../.portulan/proposals/0005-a-repository-workspace-must-declare-its-tree.md). Optional
407
+ everywhere turned out to be a hole: deleting one line degraded the whole claims-lint class to notes,
408
+ GREEN, exit 0. A `repository` workspace *is* the policy layer of a repository that is present, so it
409
+ always has an answer; `demo` and `portfolio` genuinely do not, and stay exempt.
410
+
411
+ **This was the first rule here the schema does not carry, and that is a real cost.** A conditional
412
+ dependency between two keys needs `if`/`then` or `dependentRequired`, and neither is in the subset
413
+ [`README.md`](README.md) declares — so the constraint lives in `doctor` as a cross-field check, invisible
414
+ to anyone reading the schema alone. Stated loudly rather than hidden, because "read the schema to know
415
+ the contract" is otherwise false in these places, and an unmarked exception is worse than a marked one.
416
+
417
+ **It is now one of nine**, and [`README.md`](README.md) keeps the running count because the number is
418
+ the thing to watch. Two arrived with `memory` at 2.3 — a declared `memory` object needs a
419
+ `slots.memory` store to index, and `memory.index.path` must resolve *outside* it — two more with
420
+ `librarian` at 2.4, and then that same store-and-siting pair twice again: over the handoff series at 2.5
421
+ and over the per-persona scope layer at 2.6. The two `librarian` brought:
422
+
423
+ - **`librarian` requires `slots.memory`.** There is nothing to age otherwise, and a pass over an absent
424
+ store reports that nothing is stale, which is indistinguishable from a healthy one.
425
+ - **`librarian.staleness.proposal_days` requires `slots.proposals`.** A threshold nothing can ever
426
+ cross reads as configured to anyone who greps for it. The pass itself reports *not asked* in that
427
+ case, which is deliberately not the same answer as *none pending* — but a policy that can only ever
428
+ print *not asked* is a policy nobody meant to write.
429
+
430
+ **What `doctor` checks:** the path resolves to a directory; and that a `repository` workspace has one.
431
+ With `tree` present, every path-shaped claim
432
+ in a repo card must exist, and the gate map's required-check claim must match a workflow job. With
433
+ `tree` absent, those claims are counted and **reported unverifiable** — never skipped silently, because
434
+ the whole point is that a check which vanishes without saying so is worse than one that admits it could
435
+ not run.
436
+
437
+ Three limits worth stating before somebody relies on this. A claim is resolved against **either** the tree
438
+ root or the card's own directory, because a real card mixes the two bases in one line — customer zero's
439
+ does — and a lint that insisted on one would produce false reds. Only tokens that look like paths are
440
+ checked: something containing `/`, never absolute, taken from a code span or a link target. `build: none`
441
+ claims nothing. Prose outside the two parsed sections is left alone rather than guessed at, because an
442
+ ambitious parser is the shortest route to the false red that gets a whole recipe switched off.
443
+
444
+ **Severity is where that lesson actually bit**, and the rule is worth knowing before writing a card. A
445
+ build/test/run candidate that is a **single path-shaped token** — `./verify.sh` — is an unambiguous claim
446
+ and **fails** when absent. A candidate that is a **command** — `dotnet run --project src/App` — only
447
+ *contains* tokens that might be paths, and those are **reported, never failed**, because nothing can
448
+ distinguish an input path from an output path not built yet, a flag value, a `sed` expression, a glob or
449
+ an npm script name. A line with nothing path-shaped in it is counted and reported too. Nothing is
450
+ dropped in silence — which it was, until the third real workspace's card exposed it — but the first
451
+ attempt at fixing that failed command tokens outright and produced false reds on `go test ./...`,
452
+ `cc -o bin/app src/main.c` and `--project=src/App` with the directory present, which is precisely the
453
+ shape [`../core/templates/repo-card.md`](../core/templates/repo-card.md) tells adopters to write.
454
+
455
+ And the third: **claims resolve against the filesystem, not against git.** A card naming a directory that
456
+ `.gitignore` excludes — a runtime `state/` or `logs/` — resolves in a working copy where the application
457
+ created it and is absent from a clean checkout. The failure direction is local-green / CI-red, which is
458
+ fail-closed and therefore the safe one, but it means a local green is the weaker of the two.
459
+
460
+ ## `packs` — the cascade's missing middle
461
+
462
+ Nothing in the criterion asks for this, and it is included anyway, which needs justifying. The cascade is
463
+ `core < pack < workspace < repo card < task`; every layer but one is addressable from a manifest, and a
464
+ workspace that cannot say which packs it composes leaves an agent to infer the middle of its own
465
+ resolution order. The portfolio workspace at milestone 6 ships as a private-feed plugin composing a pack
466
+ delivered by that feed — a manifest that cannot express that forces a MAJOR bump one milestone after v1.
467
+
468
+ _(This sentence said **premium** packs until milestone 6, session 1, and no shipped pack is premium: the
469
+ one the portfolio workspace composes is the universal checkpoint ritual, delivered by the private feed and
470
+ authored in the public engine. The prediction was reasonable and turned out not to be what milestone 6
471
+ built, so it is corrected to what the tree shows rather than left as a promise about a pack nobody wrote.
472
+ Whether a premium pack should exist at all is a commercial question for the maintainer, not a gap this
473
+ file may quietly keep open.)_
474
+
475
+ An array of names in cascade order, empty by default. Each name is the canonical `category/name` form
476
+ the Pack Definition owns ([`pack.schema.json`](pack.schema.json)) — but the items stay **free strings**
477
+ here, deliberately. Tightening them to that shape would be a constraint a manifest could newly fail,
478
+ which is a MAJOR bump, and the two schemas are on separate version trains precisely so neither forces
479
+ the other's hand.
480
+
481
+ **What `doctor` checks**, as of milestone 6: that the names are unique, that each **resolves** to a
482
+ `pack.json` under a resolution root, and that the manifest it finds **validates against the Pack
483
+ Definition**. It reports what each pack contributes. Resolution roots are derived from `tree` rather
484
+ than declared — a slot before its consumer is the mistake this specification was written to avoid — so
485
+ a workspace with no `tree` has nowhere to search and its declared packs are reported *unverifiable*
486
+ rather than failed, which is the same answer `tree`'s absence already gives every other claim that
487
+ needs one.
488
+
489
+ **What is still not demonstrated is resolution from a FEED.** The roots searched today are the
490
+ workspace's own tree; an adopter installing a pack from a private marketplace resolves inside the
491
+ installed plugin instead. The resolver takes its roots as an argument for exactly that reason — so the
492
+ feed case is the same code path rather than a parallel one — but it has not yet been run that way, and
493
+ this sentence is what says so.
494
+
495
+ ## `memory` — what the store's index is called, and what memory may cost
496
+
497
+ Added at 2.3. `slots.memory` is the store; this object is the machine configuration around it — the
498
+ same split as `slots.gates` (the argument, in prose) and `gates` (the policy, compiled), and for the
499
+ same reason: one is content a human reads, the other is fields a tool dispatches on.
500
+
501
+ It exists because [`memory.md`](../core/operating/memory.md) has described a *generated, size-budgeted*
502
+ index since milestone 1 and neither adjective had a machine behind it. The index is emitted by
503
+ [`../cli/index.mjs`](../cli/index.mjs), committed, and byte-compared by a verify recipe — generated so
504
+ the store is the single source, committed so a change to what is always loaded is reviewable in a diff.
505
+
506
+ | Field | What it is |
507
+ |---|---|
508
+ | `index.path` | Where the generated index is written. Must resolve **outside** `slots.memory` — see below. |
509
+ | `index.budget.lines` | The most lines the index may hold. One line per record, so this is a rail on record count. |
510
+ | `index.budget.columns` | The most columns one line may hold — refused, never truncated. |
511
+ | `store.budget.kilobytes` | The most the store's records may total, in 1024-byte KB. |
512
+ | `store.budget.record_kilobytes` | Added at 2.8. The most any **one** record may hold. |
513
+
514
+ **Four numbers rather than one, because they are different axes — and each pairs with a per-unit cap
515
+ that closes the hole the aggregate has.** The index is what gets loaded to decide what else to load,
516
+ so its line count is what memory costs on every recall. It cannot see the other axis at all: a store
517
+ whose record count never moves can grow without limit in bytes, and nothing in the index would change.
518
+ Then each aggregate has the same blind spot inside its own units. The `columns` cap closes it for
519
+ `lines` — one enormous line absorbing what the budget counts. **`record_kilobytes` closes it for
520
+ `kilobytes`**, and it is the same sentence one level down: one enormous record absorbs what the store
521
+ budget counts. Added at 2.8 with proposal `0025`, after the aggregate ran to 98% while the record that
522
+ drove it there had never once felt the rail.
523
+
524
+ **The per-record cap also fixes where a breach lands, which is the half a bigger number could not
525
+ buy.** An aggregate over individually-authored records is a **commons**. The record that grew to 15.7
526
+ KB never breached anything at write time; three unrelated changes hit the total afterwards, one with
527
+ 551 bytes of headroom. Under a per-record cap the breach is **local** — record X over its cap never
528
+ blocks writer Y — and it fires on the author growing the record, at the moment of growth. The two
529
+ forms stay independent and a workspace may declare either, both, or neither: this repository's own
530
+ workspace declares `record_kilobytes` alone, and [`../examples/workspace.json`](../examples/workspace.json)
531
+ declares `kilobytes`, so both rails have a live carrier in the tree rather than one of them being a
532
+ key the schema accepts and nothing exercises.
533
+
534
+ **Nothing is defaulted**, on the `floor` object's rule from 2.2: a default here would be this
535
+ specification setting a policy for every workspace that ever adopts it, in a key nobody typed. An
536
+ undeclared number is not checked. A number that is declared and is not a positive integer is
537
+ **refused** — `lines: 0` would otherwise read as falsy, switching the rail off in the key that exists
538
+ to switch it on, and the subset has no `minimum` with which to say so in the schema.
539
+
540
+ **What `doctor` checks:** that `index.path` resolves; that `slots.memory` is present whenever this
541
+ object is; and that `index.path` does **not** resolve inside `slots.memory`. The last is the one worth
542
+ arguing. `doctor`'s store report walks every `.md` in the store, so an index living there is counted as
543
+ a record, sized into the KB figure, and reported for stating no retirement condition — a report about
544
+ the store that includes a file the store does not hold. The alternative repair was to exempt the
545
+ index's filename from the walk, and that was rejected: an exemption by name is a door any record could
546
+ walk through, and this repository had found eight fail-opens of that shape in its own scaffolding. A
547
+ siting rule has no such door — though the rule still has to be *tested* correctly, and the first cut of
548
+ this one was the ninth: it read a leading `..` in a **filename** as a traversal, so `memory/..index.md`
549
+ was judged outside the store it plainly sits in, written there, and counted as a record. The door was
550
+ not in the design; it was in the containment test.
551
+
552
+ **What no checker establishes:** that a budget was not simply raised in the change that breached it.
553
+ That rule is [`memory.md`](../core/operating/memory.md)'s and the human gate's — refusing a raise needs
554
+ a check that reads git history, which produces false reds in a shallow CI checkout. The limit is
555
+ recorded in [`../.portulan/verify/README.md`](../.portulan/verify/README.md) with the measurement
556
+ behind it, rather than left for a reader to assume the rail covers both halves.
557
+
558
+ ## `handoffs` — an index over a series nobody can consolidate
559
+
560
+ Added at 2.5, and it is deliberately one key. `slots.handoffs` is the series; this object says where
561
+ its generated index goes. The same split as `memory` above, built by the same generator, byte-compared
562
+ by the same recipe.
563
+
564
+ | Field | What it is |
565
+ |---|---|
566
+ | `index.path` | Where the generated index is written. Must resolve **outside** `slots.handoffs` — see below. |
567
+
568
+ `index` is **required whenever `handoffs` is present**, which is the one way this object differs from
569
+ `memory`. There an object with no `index` is coherent: a workspace may rail its store's size and
570
+ generate nothing. Here there is no budget and exactly one key, so `handoffs: {}` configures nothing —
571
+ a no-op that reads as configured to anyone who greps for it, the same failure
572
+ `librarian.staleness.proposal_days` without `slots.proposals` is refused for. Unlike the two
573
+ conditional requirements below, this one *is* expressible in the declared subset, so the schema
574
+ carries it rather than `doctor`.
575
+
576
+ **Why it exists.** [`loop.md`](../core/operating/loop.md) has said since the handoff-cadence rule landed
577
+ that *"the librarian that mines the series is milestone 5"*, and the series had no index at all while
578
+ the store it sits beside had one. Measured on this repository the day the milestone-5 row was amended:
579
+ the handoff series was **3.4×** the memory store. The layer with the index was the small one.
580
+
581
+ **Every field on a line is derived, and the two carriers differ from the store's on purpose.** A
582
+ record's title is its **filename** — [`memory-entry.md`](../core/templates/memory-entry.md) prescribes
583
+ no heading, and most records carry none, so the filename is the only title every record has. A
584
+ handoff's title is its **H1**: the filename leads with an ISO date and continues as a slug, so a
585
+ filename-derived title reads `2026 07 28 the librarian goes on a cron` — a string no reader navigates
586
+ by and no cross-reference uses. Every handoff in this repository carries an H1, so the heading is the
587
+ carrier that actually exists here, and one that does not is **failed** rather than titled from its
588
+ filename: a generated line whose title the generator invented is the one thing a generated file must
589
+ not contain. The date is derived from the filename, which is where the cadence rule already puts it.
590
+
591
+ **Its siting rule has two enforcers rather than one, and neither is redundant.** An index inside
592
+ `slots.handoffs` is either counted as a handoff by `docs.sh`'s date-correspondence check — inflating
593
+ one side of a count the Session log is held to — or, carrying no date, failed by the same check for
594
+ exactly that. `index` refuses the siting outright, because it cannot render a series it would then be
595
+ a member of. The second enforcer is not a second opinion: the generator physically cannot proceed,
596
+ while the recipe is what catches a hand-placed file the generator never saw.
597
+
598
+ **There is no budget here, and the absence is the argued half.** A budget's only permitted remedy is
599
+ consolidation — merge, compress, split, retire ([`memory.md`](../core/operating/memory.md)) — and a
600
+ handoff series is append-only by construction: one per session, dated, held to the Session log by a
601
+ count-based correspondence. Retiring a handoff to buy headroom would either red that check or destroy
602
+ the record it exists to keep; **splitting one** — the move 2.8 added for the per-record rail — would
603
+ put two dated handoffs against a single session and red the same correspondence from the other side;
604
+ and raising the budget in the change that breached it is the one repair that doctrine rules out. Every
605
+ remedy such a budget could ask for is barred, which makes it a rail designed to be broken — and a rail
606
+ that fires with no legal repair is how a whole recipe gets switched off.
607
+
608
+ _That test is the one the store's own aggregate byte budget later failed — which is why **this
609
+ repository's workspace** swapped `kilobytes` for the per-record cap at 2.8 rather than widening it. The
610
+ definition replaced nothing: both keys stay legal and a workspace may declare either. The argument this
611
+ paragraph makes about handoffs is the argument `0025` makes about `kilobytes`, and it was written down
612
+ before the case arrived._ Whether
613
+ the series should be railed on some other axis is a **maintainer's question**, deferred to the
614
+ reconciliation that follows milestone 5 rather than pre-answered here in the shape of an unused key.
615
+
616
+ **What the pass reads that this object does not configure.** Record ages come from git for both series,
617
+ and the handoff series is **reported** — count, oldest, total size — with no threshold and no demotion
618
+ draft. That is not an omission: `librarian.staleness.record_days` drafts a demotion, and drafting one
619
+ against an append-only series would recommend the single repair the paragraph above rules out, weekly,
620
+ forever.
621
+
622
+ ## `personas` — where a pack-declared memory scope lands, and how anyone can tell it did
623
+
624
+ Added at **2.6**, as a pair: `slots.personas` is the adopter's per-persona layer, one directory per
625
+ persona whose memory scope a composed pack declares, and `personas.index.path` is the generated artifact
626
+ over it. The same split as `memory` and `handoffs`, built by the same generator, byte-compared by the same
627
+ recipe.
628
+
629
+ | Field | What it is |
630
+ |---|---|
631
+ | `slots.personas` | The layer. One directory per declared scope, each **empty until earned**. |
632
+ | `personas.index.path` | Where the generated index goes. Must resolve **outside** `slots.personas`. |
633
+
634
+ **Why it exists.** [`memory.md`](../core/operating/memory.md) has said since milestone 1 that memory is
635
+ per-agent rather than global, and until milestone 6 nothing in this project had a per-agent store: every
636
+ store shipped was a *workspace's*, shared by whatever agent read it. A **pack** is the first artifact this
637
+ framework distributes that carries per-persona material its adopter does not own, which is the first point
638
+ at which *whose* memory this is has an answer that matters — the maintainer's ruling of 2026-07-29,
639
+ verbatim, *"row 6 declares, row 7 validates"*.
640
+
641
+ **This series' source is the cascade, not the tree — the only one of the three.** The memory store and the
642
+ handoff series are directories the workspace owns and the generator walks. A scope is declared by a persona
643
+ *inside a pack the workspace merely composes*, so finding it means resolving `packs` the way `compile` and
644
+ `doctor` already resolve it. One consequence is worth stating because it looks like a bug the first time:
645
+ a workspace can be handed a resolution root on the command line (`--pack-root`), so the same workspace
646
+ renders the same index whether the pack came from beside it or from a private feed. Every field on a line
647
+ is content-derived, so the artifact is **path-independent** — measured at milestone 6, session 1, where
648
+ customer zero and a feed-installed adopter produced the identical scope digest.
649
+
650
+ **Declared rather than derived from `slots.memory`, and both halves of that are the argument.** Not
651
+ derived, because a path this specification computed would be Portulan choosing a location inside every
652
+ adopter's workspace in a key nobody typed — the rule every memory budget here already obeys. And not a
653
+ subdirectory of the store, for two reasons that point the same way: doctrine holds per-agent memory apart
654
+ precisely so a reviewer's recall does not spend the implementer's budget, and a store nested inside the
655
+ store would be counted by the `kilobytes` rail while being invisible to the flat walk that counts it —
656
+ [issue #76](https://github.com/sleepy-panda-srl/portulan/issues/76). Siting the layer outside leaves that
657
+ issue open to be decided on its own merits instead of settled as a side effect of this one.
658
+
659
+ **A location is literally empty, and that is a ruling rather than a reading.** *"Present and empty"* could
660
+ mean *empty of records* — a marker file inside stating whose scope it is — and the maintainer ruled on
661
+ 2026-07-30 that it binds literally. Which has a consequence the design leans on: **git does not record an
662
+ empty directory**, so the location cannot travel in a plugin payload at all. The declaration travels in the
663
+ index; the directory is created in the adopter's own layer when the adopter runs the landing. An absent
664
+ location is therefore never a finding — it is the state of every fresh clone.
665
+
666
+ **Two controls, and the second one is what makes the first mean anything.** The index is the *positive*
667
+ control: every field on a line is derived from the pack, including a digest over the declared scope's own
668
+ text, so a pack that rewords a scope moves this file and the byte comparison tells the adopter. A first
669
+ sentence alone would have let a pack reword everything after it invisibly. The *negative* control is a
670
+ sweep for **anything** under the layer that no composed persona declares — a stray file as much as a stray
671
+ directory, since the first implementation tested only directories and let a `.md` dropped in pass in silence.
672
+ Without it the layer would accept anything and the positive control would only ever examine locations it
673
+ already expected, which is
674
+ [`a-manifest-field-can-validate-and-load-nothing`](../.portulan/memory/a-manifest-field-can-validate-and-load-nothing.md)
675
+ wearing this feature's clothes. A third check runs at the *distributing* side: a pack shipping memory
676
+ records of its own is refused, because storage follows ownership and a pack absorbing an adopter's records
677
+ is the constitution's thesis 6 violated from the direction the adopter cannot see.
678
+
679
+ **What no part of this reads, said plainly.** Nothing recalls from these locations, nothing consolidates
680
+ them, and no budget rails them. `doctor` validates a persona against its five-part contract **as of
681
+ milestone 7 session 2** — that is the *validates* half of the ruling above, and it arrived rather than
682
+ being promised. What it validates is the **presence** of the five parts, including a `## Memory scope`
683
+ section; it does not read these locations or judge what a scope contains, which is why this paragraph's
684
+ heading still holds. There is no budget for the same reason:
685
+ there is nothing yet to budget, and the axis such a rail should use is per-persona rather than
686
+ per-workspace, which belongs to the row where something finally reads these locations.
687
+
688
+ ## `provenance` — a record field, not a manifest key
689
+
690
+ The criterion lists a provenance slot beside the others, but it does not belong in the manifest, and
691
+ saying why is more useful than quietly filing it somewhere. Provenance is a property of **a rule**, and
692
+ rules do not live in the manifest — they live in memory entries and proposals, which are Markdown. A
693
+ manifest key would have described a workspace's *policy about* provenance while leaving every actual
694
+ rule unchecked.
695
+
696
+ So the schema defines the **shape** in `$defs/provenance` and the records carry instances of it:
697
+ one definition, two carriers. `doctor` parses a record's `provenance:` field into that shape and
698
+ validates it.
699
+
700
+ Adopted 2026-07-25 by the maintainer from
701
+ [proposal 0002](../.portulan/proposals/0002-sealed-provenance.md). Two forms, one of them mandatory:
702
+
703
+ ```
704
+ provenance: form=link href={ticket | PR | postmortem | commit}
705
+ provenance: form=sealed owner={who can re-validate} date={YYYY-MM-DD} shape={the de-identified failure}
706
+ ```
707
+
708
+ **Why two.** Thesis 4 requires every rule to link to the incident that created it; thesis 6 requires a
709
+ team's specifics to stay in the layer their owner controls. A rule generalised upward out of a private
710
+ incident collides with both — it must either carry a link it is not allowed to hold, or drop provenance
711
+ and become un-retirable. The sealed form is the declared-weak option: the stamp and the mechanism travel,
712
+ the episode does not.
713
+
714
+ **Honest limits, carried from the proposal rather than lost in adoption:**
715
+
716
+ - **The machine checks the stamp's form, never its truth.** A fabricated sealed stamp passes exactly as a
717
+ real one does. What guards the content is the *generic must never decay into vague* bar in
718
+ [`codify` step 1](../core/skills/codify/SKILL.md) plus human review — the machine catches absence, the
719
+ human judges substance.
720
+ - **"Resolvable" means well-formed, not fetched.** `doctor` validates a link's shape and does not
721
+ dereference it. A gate that needs the network fails for reasons unrelated to the change under test, and
722
+ a flaky gate is worse than no gate — this workspace already refuses network-dependent checks.
723
+ - **A sealed rule is not as good as a linked one.** It is *declared* weak instead of silently absent.
724
+ `doctor` therefore reports the **sealed proportion**: a workspace where everything is sealed has
725
+ quietly opted out of retirement altogether, and that is a health signal rather than a curiosity.
726
+ - **Adopting this was a constitutional interpretation**, and the constitution has since been changed to
727
+ match. It decides how thesis 4's "links to the incident" reads in the collision case; the maintainer
728
+ accepted it, reserved the matching wording change in [`../docs/vision.md`](../docs/vision.md) to his
729
+ own hand, and made it. Thesis 4 now names both forms itself, so the machine that enforces the two-form
730
+ shape is enforcing something the constitution states — which is the only order these two are allowed to
731
+ be in. _(For a window between the two changes the constitution read "links" while this spec permitted a
732
+ stamp. That gap was recorded here rather than smoothed over, and closed before `doctor` shipped, which
733
+ is what the proposal required: a rule mechanically enforced by tooling and absent from the constitution
734
+ is backwards for this product.)_
735
+
736
+ - **It binds a rule, and only a rule.** `doctor` requires a two-form stamp on `type: rule` records in the
737
+ `memory` slot and nowhere else. A `decision`, `reference`, or `glossary` record whose provenance parses
738
+ as neither form is **reported**, never failed — thesis 4, this proposal as adopted, `dod.md` condition
739
+ 3, and the milestone's own criterion are all rule-scoped, and having the validator bind types nobody
740
+ legislated for would be the same inversion as the previous bullet. **Proposals are not checked at all**
741
+ in v1, though a proposal argues for a rule and carries a `Provenance` section of its own. Two reasons,
742
+ and the second is the one that decides it. A proposal is the *argument* for a rule; the rule itself
743
+ lands in a memory entry or in `core/` when accepted, and that is the carrier the mandate names — a
744
+ proposal's provenance documents where the reasoning came from, which is a different thing from the
745
+ incident stamp `$defs/provenance` describes. And at any moment a proposal can be sitting
746
+ **pending** the human gate, so a checker demanding a format from it would be acting on a record
747
+ mid-decision, which is the gate's business and not a validator's. Saying which carrier is unchecked
748
+ matters more than the choice does — silence is how the next reader assumes coverage that is not there.
749
+
750
+ - **`type` is self-declared, and that is an opening.** A rule labelled `decision` walks past the check.
751
+ Closing it means either inferring type from content or extending the mandate, and the first is
752
+ guesswork while the second is the maintainer's to legislate, not the validator's to assume.
753
+
754
+ ## Considered and left out
755
+
756
+ Each of these was a candidate; none is an oversight.
757
+
758
+ - **`rituals` as its own slot.** The constitution names rituals as a pack category, and — when this was
759
+ written — also inside thesis 1's workspace list. The two readings disagreed and the schema had to pick
760
+ one. It picks packs: rituals ship as packs ([`../packs/rituals/`](../packs/rituals/)), so a workspace
761
+ composes them through `packs`, and a second slot would be two ways to say one thing. **The ambiguity
762
+ was raised with the maintainer rather than settled here** — the constitution is human-owned and a
763
+ schema is not the place to decide what it means — and he has since settled it: thesis 1 now lists
764
+ rituals once, under packs, which is the reading this schema had guessed at. Kept rather than deleted,
765
+ because the record of *how* the disagreement was resolved is the part worth having: the schema deferred
766
+ and the human legislated. Revisit if a workspace-local ritual ever needs a home that is not a pack.
767
+ - **`stack` and `glossary` as slots.** Both are named in thesis 1 and both live inside `identity.md`
768
+ today. Splitting them buys nothing until something *consumes* them — a stack pack selecting itself
769
+ from a declared stack would be that consumer, and it does not exist. Splitting on speculation is how a
770
+ schema acquires slots nobody fills.
771
+ - **A sealed-owner registry.** Sealed stamps name an owner as free text; a workspace-level list of valid
772
+ owners would let `doctor` catch typos. Deferred: it adds a required-to-maintain list for a check that
773
+ catches a mild failure. The librarian's nagging is the mechanism that would actually need it, and as
774
+ of milestone 5 it exists and reads the owner as free text — so this is a deferral with a live consumer
775
+ rather than a speculative one, and the first typo'd owner is the incident that would settle it.
776
+ - **A per-host capability matrix.** The vision's `doctor` includes a per-host capability report, but that
777
+ is generated from the host and the compiled gates, not declared by the team. It belongs with the
778
+ enforcement backends in milestone 4.
779
+ - **`evals`.** Milestone 8 owns the eval harness; a slot pointing at golden tasks before any exist would
780
+ be the emptiest kind of scaffolding.
781
+
782
+ ## What v2.0 is not
783
+
784
+ - **It has been validated against exactly two instances**, one of which it was derived from. The demo in
785
+ [`../examples/`](../examples/) was the real test, and it did what a second instance is supposed to do:
786
+ it produced a schema change (`tree`) within an hour of being written. A third differently-shaped
787
+ workspace — the portfolio one at milestone 6 — should be expected to produce another.
788
+ - **`doctor` checks form, not truth.** Every line above says what it resolves, parses, or matches. It
789
+ cannot tell whether a mission statement is still accurate, whether a sealed stamp describes a real
790
+ incident, whether a gate map's tiers are honoured, or whether a `requires` list is complete. The
791
+ machine catches absence; the human judges substance.
792
+ - **It never executes anything.** Recipes are read and never run. The Stop-gate runner (milestone 4)
793
+ executes the **default** recipe, so that one is tested by being run; every other declared recipe is
794
+ still read only, and one quietly needing an undeclared tool still passes.
795
+ - **Nothing here describes how a workspace is *installed*.** Workspaces ship as plugins through a feed;
796
+ that packaging is milestone 3 for the public path and milestone 6 for the private one.