feat: cross-model imports, slice 1 (local imports) - #34
Conversation
Review round 1 — adversarial pass (approximation, not
|
| M-1 | mdCell ran after link assembly, escaping | inside the URL → dead link |
| M-2 | a.b.C, .C, payments. were silent; the plausible typo payments.v2.PaymentMethod passed |
| M-3 | runImports ignored structuralOK, then advised impossible syntax ("reference … as .Name") |
| M-4 | an imported model's version was never checked; v99 was silently mined |
| L-1 | raw newline broke line-wise diagnostic output |
| L-2 | three scope regexes disagreed on minimum length; a.modelith.yaml was unimportable |
| L-3 | one unbound scope produced 201 errors; now one finding with a count |
| L-4 | a duplicate scope was masked by an unreadable second binder |
L-2's fix is worth a reviewer's eye: model.ScopeSlug is now the single definition, guarded by TestInvariant_ScopeSlugMatchesSchema.
M-5 — held back as a design decision, then implemented (9f4d4c9, ADR-0013)
Import paths could walk anywhere, and the four distinct outcomes formed an existence-and-permission oracle. Resolution is now confined to the enclosing repository — the nearest ancestor with a .git entry — falling back to the model's own directory when there is none.
Verified by hand: an import escaping the repo is rejected, and so is a prefix-sharing sibling (repo-evil beside root repo), which is exactly what a strings.HasPrefix containment check would have admitted. The oracle's three distinct answers now collapse to one diagnostic.
Three traps are covered by tests, each verified by mutating the implementation until the specific test failed: .git is a file in a linked worktree; symlinks resolve before containment; containment uses filepath.Rel.
The agent also caught an error in my spec — I had written that a prefix-sharing sibling should "resolve", which describes the bug rather than the fix. It implemented the rejection and flagged the discrepancy instead of following the spec off a cliff.
Filed separately
#35 — a backtick in attributes[].name, attributes[].type, an action name or a role breaks out of its code span. Pre-existing on main, not from this PR, so it is not fixed here. Sibling of #29.
Still needed
task check green, hack/mermaid-check.sh clean (17 blocks), goldens regenerated and diffs read. Staying in draft until /code-review high has run — this touches schema/struct sync, lint rules and renderer determinism, and an approximated round is not grounds to flip a correctness-critical change to ready.
Review loop — round 1 recordSource: real Seven findings. F1–F6 carry executable repros (CONFIRMED); F7 is a UX nit.
Design calls made before the fix round
Checked and cleanContainment against Fix round in progress. Staying in draft until it clears. |
Review loop — round 1 fixes, independently verifiedAll seven findings fixed across 8 signed-off commits (
Verification — repros run against base and HEAD binaries, not taken on trustF1 —
F2 —
Containment was not loosened by the F1 fix — both escapes still rejected on HEAD, and now naming
That second case also demonstrates F6: the qualified-ref error and the import error both appear in one run. F4 — real
F5 — F7 — renders as Judgment calls recorded rather than guessed (both reversible)
Still openThe fix round added a new schema field and refactored the containment seam — a new surface that round 1 never saw. Staying in draft pending a real |
Review loop — round 2 recordSource: real No regressions from the round-1 fixesThe reviewer independently validated the containment logic it had flagged in round 1: the lexical Neither of the round-1 design calls (F3's unconditional dotted-type check, F4's New findings
R2-2 is the notable one: it is a downstream consequence of the F3 call from round 1. Keeping the dotted-type check unconditional was right, but the shipped skill is what agents actually read, and it was left describing the old contract. Fix round 2 to follow. Staying in draft. |
Review loop — round 2 fixes, independently verifiedAll five round-2 findings fixed across 3 signed-off commits (
Verification — repros run against the built branch binaryR2-1 — rendered Both link kinds relativized, not just the Imports list. The target resolves to a real file once the import is rendered in place. Semantics implemented, since the CLI cannot know what filename an imported model was rendered under: target = the imported model's default rendered path, expressed relative to the output file's directory. It resolves whenever the import was rendered in place, which is the documented convention. It does not assume the import was rendered into the output directory. R2-3 — model importing R2-4 — same model at R2-2 — five type forms checked against the binary, each matching what the skills now claim:
Loop statusTwo review rounds done, both real user-invoked Round 2's fixes changed a renderer signature and rewrote three shipped skills — a surface no review has seen. Under |
A model may declare a kebab-case `scope:` slug and list `imports:` — paths to peer model files, relative to itself — so an item defined elsewhere can be referenced as `slug.Name` (ADR-0010). Both are optional; the slug is declared by the imported model and never restated by the importer. Schema and structs move together, per TestSchemaStructSync. Signed-off-by: Joe Beda <joe@stacklok.com>
lint.Run now takes the model's path and a one-method FileReader seam so it can read the peer models an `imports:` list names. fs.FS was rejected: fs.ValidPath forbids "..", which peer models in a sibling directory require. Resolution is non-recursive (ADR-0010): only items defined directly in a listed file are reachable, so mutual imports terminate with no cycle detection. An unresolvable qualified type is an error, where an unqualified PascalCase type that names no enum stays a warning — it may be a primitive the author invented, while scope.Name can only be a cross-model reference. A qualified value in an entity position (relationship.entity, subtypeOf) gets a friendly error ahead of the schema's opaque pattern violation, which is then suppressed so one mistake reads as one finding. Signed-off-by: Joe Beda <joe@stacklok.com>
Adds an Imports section to the schema reference covering the two new fields, the non-recursive resolution rule, the entity-position deferral, and why an unresolvable qualified type is an error while an unqualified PascalCase one is only a warning. Amends the bounded-contexts omission, which claimed the format had no cross-context reference at all. Signed-off-by: Joe Beda <joe@stacklok.com>
…ed file Supersedes the binding introduced in 04d34be. A model no longer declares a top-level `scope:`; each `imports:` entry binds one instead, defaulting to the file's basename with .modelith.yaml stripped, with an explicit {scope, path} object for a filename that is not a usable slug or is already bound. The union follows the precedent `actions` set. The cause was the renderer. Its links into an imported model's Markdown need to pair a scope with a path, and it may not open imported files to learn that pairing, so a scope declared by the imported file was unresolvable where it was needed most. Moving the binding to the importer makes it local, and the renderer needs no filename convention and no guessing. Falls out of it: the imported-file-declares-no-scope error is gone, and the unreferenced-import finding becomes a completeness one, alongside the unused enum and glossary term whose promotion under --completeness error it shares. Recorded as ADR-0012. Signed-off-by: Joe Beda <joe@stacklok.com>
ADR-0012 supersedes ADR-0010's identity decision and the part of its resolution decision that made the imported file the source of truth for the slug; both originals are left untouched. The schema reference now documents the union form, the filename default, and the local nature of the binding. Signed-off-by: Joe Beda <joe@stacklok.com>
Adds docs/05-parking-garage/payments.modelith.yaml — a small payments context owning the PaymentMethod enum — and has the garage import it for Ticket.paidWith rather than keeping a second copy of the same list. Both lint clean under --completeness error and both .md files are committed; the directory is globbed by path, so CI picks the new model up with no change to Taskfile.yml or ci.yml. The imports material sits at the end of index.md, marked advanced: most readers will not need it for a long time and the teaching flow above it is the point of the page. Signed-off-by: Joe Beda <joe@stacklok.com>
An import path reaches a published Markdown page unescaped, in both the link label and the link destination, while every other string in the renderer goes through mdCell/oneLine. A path closing the destination and opening a tag renders a live element and a second link from a model that lints clean; a path holding a newline injects a heading into the Imports list and splits a lint diagnostic across two lines. Escape each value for the position it lands in. A link destination is percent-encoded outside the URL unreserved set, so parentheses, whitespace, angle brackets and control characters cannot end it. A link label is backslash-escaped plain text rather than a code span: a code span cannot escape the "]" that ends the label, which would leave its inertness resting on the rule that code spans bind more tightly than link brackets — the spec says so and widely used renderers disagree. A code span, where one is still used, is fenced past the longest backtick run inside it. oneLine now replaces every control character, not only the line breaks. The parts are escaped before the link is assembled; escaping the finished link is what left a pipe inside a URL as "\|". Alongside, the lint layer: - an import path holding a control character is an error, and every import diagnostic quotes the path with %q so an embedded newline survives as text instead of a line break; - an attribute type containing a dot must be exactly scope.Name, and a malformed one says which way it is malformed rather than passing as a primitive; - imports resolve only against a document the schema accepted, so a rejected scope no longer binds and then advises impossible syntax; - an imported model declaring an unsupported schema version is an error naming the file and the version; - references to one unbound scope are reported once with a count, and a scope whose import failed to load is not blamed at every reference site — a single typo was producing 201 errors; - a duplicate scope binding is settled before the file is opened, so an unreadable second binder no longer masks it. model.ScopeSlug becomes the one definition of a scope slug, consumed by the linter, the renderer and the schema, which disagreed on the minimum length: a single-character scope is now legal. The only golden change is the imports bullet, whose path label loses its backticks. Signed-off-by: Joe Beda <joe@stacklok.com>
An import path could walk anywhere. "../../../../etc/passwd" was read, and the four outcomes a read produces — resolves, missing, unreadable, holds no model — are distinguishable, so they form an existence and permission oracle over the whole filesystem. modelith ships an Action that lints models in CI, so a *.modelith.yaml from a fork could probe the runner and report what it found through diagnostic text landing in a pull-request check. Resolution is now bounded by the nearest ancestor of the linted model holding a .git entry, or by the model's own directory when there is no repository above it, since outside one the tool cannot know how far the project extends. Containment is decided before the file is opened, because opening it is the leak. The three previously distinct answers on the oracle fixtures collapse to one diagnostic whose only variable is the path the author wrote. Three details decide whether this works at all, each pinned by a case that fails when the behaviour is removed: - .git is tested for existence, not for being a directory. In a linked worktree and in a submodule it is a regular file holding a gitdir pointer, and requiring a directory would collapse the root to a single directory in every such checkout — including the worktree this was written in. - Symlinks resolve before the containment test, including a dangling one, which is followed via its recorded target. A link is judged by where it points, not by where the link file sits. Leaving a dangling link unresolved would keep the oracle alive, since "unreadable" is one of the four answers. - Containment is filepath.Rel, not strings.HasPrefix, which would place "/repo-evil" inside "/repo". No flag names the root, so no diagnostic advises one; a test asserts the string never appears. Recorded as ADR-0013, with the boundary and the no-repository fallback documented in the schema reference. The oracle is not closed within the repository, and that is stated rather than engineered against: anyone who can commit a file to your repository has better options than a lint diagnostic. Signed-off-by: Joe Beda <joe@stacklok.com>
resolutionRoot climbed the ancestor chain of the path as written while every candidate import was judged after symlink resolution, so a model reached through a symlinked directory was assigned a root from a different tree: a peer in the same directory was refused, and the diagnostic named a repository that does not hold the model. Seed the walk with the resolved directory instead. The two cases added to TestADR_0013_ImportsConfinedToTheRepository — a peer import through a link into another repository, and an escape from that same location — both fail without the change. Signed-off-by: Joe Beda <joe@stacklok.com>
The FileReader seam covered reads only: resolutionRoot, realPath and absolute called os.Lstat, filepath.EvalSymlinks and os.Getwd directly, so a caller that supplied its own reader still had containment judged against the local disk. The test suite inherited that. TestImports_Resolution/parent-relative_import_resolves passed only because internal/lint happens to sit under the repository's real .git; the same tests fail in a copy of the tree without one — a source tarball, a vendored module, a checkout CI exported rather than cloned. Widen the seam to the three questions import resolution asks: read the file, where is resolution confined, and where does this path land. FileReader becomes Files; OSFiles answers all three from the local filesystem, and the test fake answers them from its own map, with a ".git" key marking a repository just as the entry does on disk. TestImports_ContainmentUsesTheFileSeam pins the behaviour that used to depend on the ambient tree: with a marker a sibling directory is reachable, without one resolution stops at the model's own directory. `go test ./...` now passes in a copy of the tree with .git removed. Signed-off-by: Joe Beda <joe@stacklok.com>
…rence reportQualifiedEntityRefs writes its findings before schema validation, and runStructural counted them when deciding whether the document was structurally valid. A model carrying both `subtypeOf: payments.Payment` and a broken import therefore reported only the former: the nonexistent import, and every unreferenced-import finding with it, stayed hidden until the unrelated mistake was fixed, at which point a second round of errors appeared unannounced. Judge structural validity by what the schema said, and let the cross-model entity reference — a supported-feature limit, not a shape the imports list depends on — stand as its own finding. Both layers now surface in one run. Signed-off-by: Joe Beda <joe@stacklok.com>
ScopeFromPath stripped ".modelith.yaml" whole but fell back to trimming only the final extension, so "./payments.modelith.yml" bound the scope "payments.modelith" — not a valid slug. RenderedPath and the render command's default output path both treat .yml as a model file, so the bare import form was the one place the spelling was not first-class, and it failed twice: an invalid-slug error on the import, then an unbound-scope error at every reference site, since an invalid slug never claims its scope. Strip both spellings whole. Covered at the unit level in TestScopeFromPath and end to end by a resolution case importing a .modelith.yml peer. Signed-off-by: Joe Beda <joe@stacklok.com>
…ally A model that exists to be imported cannot pass --completeness error. The references that justify its enums and glossary terms live in the files that import it, which it cannot see, so every enum it publishes collects "defined but no attribute uses it" and CI fails. The docs recommend factoring exactly such a model out; docs/05-parking-garage/payments.modelith.yaml escaped only because Payment.method happens to use the enum locally. Add a top-level `shared: true`. It is the counterpart of `imports` — one model declares what it reaches for, the other declares that it is reached for — and relaxes exactly the checks about a definition nothing in this file uses: the unused enum and the unused glossary term. It relaxes nothing about content: an entity with no invariants, or one no scenario exercises, is a gap in the model itself, which being imported does not fill. Schema property, struct field (TestSchemaStructSync guards the pair) and the schema reference land together, and the parking-garage payments model now declares what it already is. Signed-off-by: Joe Beda <joe@stacklok.com>
checkQualifiedTypes runs whether or not a model has `imports:`, so any dot in an attribute type is now an error — `decimal(10.2)` and `google.protobuf.Timestamp` included. That is deliberate: an unconditional rule is what makes a typo in a cross-model reference reportable instead of readable as an exotic primitive. It was documented in the schema reference and nowhere in the JSON Schema, which is the contract an editor fetches through the $schema header, and which still described `type` as a primitive or an enum name. Say it in both, and pin the unconditional part with two resolution cases in a model that imports nothing. Signed-off-by: Joe Beda <joe@stacklok.com>
The Imports bullet rendered as [./payments.modelith.yaml](./payments.modelith.md): a link that says .yaml and lands on Markdown. Show the source path as a code span and label the link for what it leads to, so the two files are two things on the page. mdLinkText was the imports label's escaper and had no other caller, so it goes with it; the code span carries the same author text with the fence-widening the rest of the renderer already relies on. The hostile-path test now checks the stronger property directly — strip the code spans from a bullet and the fixed skeleton is all that is left. Signed-off-by: Joe Beda <joe@stacklok.com>
The walkthrough tells the reader the imported model "needs no changes at all", which is true of the reference itself and now sits next to a payments model carrying shared: true. Name the one thing an imported model may want to say, and why this one says it. Signed-off-by: Joe Beda <joe@stacklok.com>
render -o <dir>/x.md and --stdout emitted import links relative to the source directory regardless of where the output landed, so a link built for outdir/main.md pointed at outdir/payments.modelith.md, which modelith never wrote. The link target is now the imported model's default rendered path (RenderedPath resolved against the source directory) expressed relative to the actual output directory; this resolves whenever the imported model was itself rendered to its default location. --stdout has no output file to relativize against, so its links stay source-relative, as they would from a default render. The common case (no -o, both directories equal) is left byte-identical to avoid gratuitous golden churn; only render -o and --stdout change. Narrows defaultOut's comment, which previously claimed the renderer's links and defaultOut "cannot disagree" unconditionally. Round 2 finding R2-1: .scratch/reviews/imports-slice1.md Signed-off-by: Joe Beda <joe@stacklok.com>
…rts layer Two related findings, one fix: reportQualifiedEntityRefs is the sole authority on cross-model references in an entity position (relationship.entity, subtypeOf) — unsupported there, but a real reference to the scope all the same. R2-3: runImports didn't know that, so an import referenced only from an entity position got both the "not supported in an entity position" error and a contradictory "never referenced — drop it" completeness warning. reportQualifiedEntityRefs now also returns the scopes it found, and runImports treats them as used, same as an attribute type would. R2-4: runSemantic and runSubtypes independently skip a value matching the qualified-reference pattern, trusting reportQualifiedEntityRefs already reported it — but runStructural only called it after the unsupported-schema-version early return, so on an unsupported version the reference went unreported by anyone: neither the skipped structural finding nor the semantic/subtype checks that deferred to it. It now runs before that return too, so the trust the skip relies on always holds. Round 2 findings R2-3, R2-4: .scratch/reviews/imports-slice1.md Signed-off-by: Joe Beda <joe@stacklok.com>
domain-model-author still described the pre-imports attribute `type`
rule ("a primitive in lowercase, or the PascalCase name of a defined
enum") with no mention of the dot reservation, `imports:`, or
`shared:`. An agent following it could emit `decimal(10.2)` or
`money.Amount` and hit an error the skill gave it no way to
anticipate. Verified against a build of this branch: `type:
"decimal(10.2)"` lints clean on main and errors here.
Adds `shared:` and `imports:` conventions to domain-model-author,
narrows the attribute-type bullet to state the dot reservation and the
cross-model-reference form, and updates domain-model-lint's structural/
semantic/completeness breakdown to include the entity-position
cross-model-reference error, the import-resolution errors, the
malformed-type error, and how `shared: true` narrows the completeness
checks it relaxes. domain-model-context gets one line: an `imports:`
entry means the vocabulary a `scope.Name` type names lives in another
file, worth loading too.
Round 2 finding R2-2: .scratch/reviews/imports-slice1.md
Signed-off-by: Joe Beda <joe@stacklok.com>
domain-model-lint/SKILL.md's semantic-error list for imports omitted the control-character and unsupported-schema-version cases that docs/06-schema-reference.md gained in the previous round. Verified both error strings against the built binary rather than paraphrasing from the docs. Round 3 finding: .scratch/reviews/r3-docs.md Signed-off-by: Joe Beda <joe@stacklok.com>
internal/lint/lint.go:195 declared a new, block-scoped entityScopes with := on the unsupported-version early-return path, shadowing runStructural's named return instead of assigning it. The explicit return false, entityScopes right after still returned the correct (shadowed) value, so this was latent, not live -- but a bare return relying on the named return would have silently returned nil. Added TestRunStructural_UnsupportedVersionReturnsEntityScopes, a white-box test calling runStructural directly (Run() never reaches this path with structuralOK true, so nothing downstream currently observes the value). Confirmed empirically that this one-line hunk alone is not observable by reverting it: the test still passes, matching the round-3 finding's own characterization. It does fail against the documented risk (shadow + relying on the named return), verified separately and not left in the tree. Round 3 finding: .scratch/reviews/r3-lint.md Signed-off-by: Joe Beda <joe@stacklok.com>
importLinkTarget joined an import path against sourceDir without checking filepath.IsAbs, so an absolute import path (a lint error, but render only runs structural validation, so still reachable) produced two different link shapes for the same model: unchanged under the default output location, silently re-rooted under sourceDir when -o pointed elsewhere. Return the rendered path unchanged when it's absolute, alongside the existing outDir == sourceDir shortcut. Added TestRenderImports_AbsolutePathIsNotRelativized, confirmed to fail against the pre-fix code (reverted the guard, verified the sibling-output-dir case relativizes to a bogus ../models/etc/... link, then restored). Round 3 finding: .scratch/reviews/r3-render.md Signed-off-by: Joe Beda <joe@stacklok.com>
2c8f3aa to
e93ab27
Compare
Review loop — round 3 recordSource: an APPROXIMATION of Three finder angles, read-and-run only:
Findings — no blockers
R3-2 is hygiene, not a live bug, and the fix agent said so rather than claiming a regression test it didn't have. Verified: the bare-return hazard is compile-blocked today either way, because Kept anyway — it is more correct, and the protection matters if that Adversarial pass21 attack cases, base vs HEAD binaries built outside the repo: sibling/nested/no-shared-ancestor output dirs, relative and The lint finder ran positive controls, so its clean result has teeth: a genuinely-unused import alongside an entity-position-referenced one is flagged only for the unused one; malformed qualified values never reach the scopes map; three runs byte-identical. One out-of-scope note it raised — a Verified independently before flipping
Rebased onto
|
| Before rebase | After rebase | Subject |
|---|---|---|
04d34be |
edc4732 |
feat(schema): add scope and imports to the v1 format |
f62effb |
2546612 |
feat(lint): resolve local imports and qualified attribute types |
c3feb8f |
840a9e1 |
docs: document scope, imports, and cross-model references |
eea7699 |
9399bdd |
feat(schema)!: bind an import's scope in the importer, not the imported file |
d31bfae |
3dfba31 |
docs(adr): record ADR-0012 and align the schema reference |
61e7e5f |
e779cb1 |
docs(example): reference a peer payments model from the parking garage |
45443af |
df2616e |
fix(imports): escape import paths and tighten the reference diagnostics |
9f4d4c9 |
83cfc2c |
feat(lint)!: confine import resolution to the enclosing repository |
a71337f |
8f62920 |
fix(lint): take the resolution root from the model's real directory |
164bbca |
8c5e729 |
refactor(lint): put import containment behind the file seam |
c717ed3 |
ffcd1fd |
fix(lint): report the imports layer alongside a qualified entity reference |
72d2ec2 |
7c42c45 |
fix(model): derive an import scope from a .modelith.yml file too |
c53237a |
4935a3a |
feat(schema): mark a model shared so its vocabulary may be unused locally |
20cef93 |
44571aa |
docs(schema): say a dot in an attribute type is reserved |
fedfb3d |
1318c66 |
fix(render): stop labelling the imports link with the source path |
6ddbeb2 |
f9cd4f6 |
docs(parking-garage): say what shared: true is for where it comes up |
1a4fcfc |
6fdbd62 |
fix(render): relativize import links against the output directory |
24572a4 |
01c3899 |
fix(lint): make entity-position cross-model refs honest with the imports layer |
e07eebd |
7cff4cf |
docs(plugin): bring the skills up to the imports/shared contract |
eedbb36 |
8de4124 |
docs(plugin): add two import error classes to domain-model-lint's list |
e7b3611 |
bf84a8b |
fix(lint): assign the named return instead of shadowing it |
2c8f3aa |
e93ab27 |
fix(render): don't relativize an absolute import path |
Loop closed
Three rounds, the cap. Round 1: 7 findings, 2 blocking. Round 2: 5 findings, 0 blocking, no regressions from round 1. Round 3: 5 findings, 0 blocking, 3 fixed and 2 deliberately declined. No blocking issues remain open. Marking ready.
Note for whoever merges: rounds 1 and 2 were real reviews; round 3 was not. If you want a real /code-review high #34 over the final rebased state, that has not been run.
…#35) Two gaps in the same class, both reachable from any unconstrained string. A backtick in a code-spanned field closed its span early and dropped the remainder into the document as live Markdown/HTML (#35). The `codeSpan` helper from #34 already fences past backtick runs, so this applies it at the call sites that wrap a schema-unconstrained value: attribute names, enum value names, action names, and an action's actor. Entity, enum and glossary keys, `subtypeOf`, a relationship's target and an invariant id are pinned by schema patterns that exclude a backtick, and `render` validates structurally before it renders, so they stay as they are. Table cells route through a new `codeCell`, since a pipe in a name ended the cell. GFM recognizes a backslash-escaped pipe inside a code span. Separately, prose fields (`definition`, `description`, a role, a note, an invariant statement, a scenario step) emitted raw HTML. They stay Markdown -- models backtick their entity names throughout -- but an angle bracket in one is now a character reference, so a tag renders as the text the author typed. The pass is code-span aware: inside a span `<x>` is already literal and escaping it would put a visible "<x>" on the page. An ampersand is escaped only where it introduces a character reference. ADR-0014 records the contract and the reasoning. No committed golden changed: no example holds a backtick or an angle bracket in any affected field. Fixes #35 Signed-off-by: Joe Beda <joe@stacklok.com>
Implements slice 1 of #25: a model can reference an item defined in another model in the same repository. Design is ADR-0010, amended by ADR-0012 (included here).
Not in this change: provenance headers, surface digests,
modelith depscommands, anything touching the network, and qualified references inrelationship.entity/subtypeOf. Those are slices 2 and 3.The shape
The importer binds the scope. There is no
scope:field in the format. This reversed a decision mid-implementation: the original design had each file declare its own slug, which the renderer then could not resolve without opening the file — and the renderer is forbidden from opening imported files, because its output must stay a pure function of the model being rendered. ADR-0012 records the reversal; ADR-0010 and ADR-0011 are untouched, per the never-edit-in-place rule.The string-or-object union mirrors
actions, which is already a bare string or an object.Behaviour
Errors: a qualified type whose scope is not bound, or which names no enum in that file, or which resolves to a non-enum; two imports binding the same scope; a bare-form import whose stem is not a valid slug (the message says to use the explicit form); an import path that is absolute, unreadable, or not a model.
Completeness finding (so
--completeness errorpromotes it, like unused enums): an import that is never referenced.The existing unqualified behaviour is unchanged — an unqualified PascalCase type that matches no enum is still a warning, because it might be a primitive the author invented. A qualified name has no such excuse. The asymmetry is deliberate and documented.
Notable
lint.Runnow takes a path and aFileReaderseam. It previously took bytes only, with no way to locate or read a sibling.fs.FSwas rejected becausefs.ValidPathforbids.., which the peer-directory layout requires. Tests use a map-backed fake, per.claude/rules/testing.md../payments.modelith.md#paymentmethod) purely from what the importing model declares.docs/05-parking-garage/, which is globbed by path inTaskfile.ymlandci.yml, so no glob edits were needed. The imports material is an advanced section at the end ofindex.md.TestADR_0009_NoSelfModel's allowlist grew by one, as designed.Review status
Draft.
task checkgreen andhack/mermaid-check.shclean (17 blocks), but the review loop has not run yet.Flagged by the implementer for a reviewer's eye:
Import.ScopeFromPath(json:"-") exists so the invalid-slug error fires only on the bare form. It is the one piece of state on a format struct that the schema does not describe.Refs #25
🤖 Generated with Claude Code