Skip to content

feat(reference): publish what an artifact CONTAINS, not just that it has a shape - #663

Open
beyondnetPeru wants to merge 4 commits into
mainfrom
feat/core-publishes-artifact-fields
Open

feat(reference): publish what an artifact CONTAINS, not just that it has a shape#663
beyondnetPeru wants to merge 4 commits into
mainfrom
feat/core-publishes-artifact-fields

Conversation

@beyondnetPeru

Copy link
Copy Markdown
Contributor

Closes the half of the artifact registry a satellite could not use.

The gap

The registry names an artifact and points at its schema's $id. A consumer therefore learns that a PRD is required in discovery and still cannot find out what a PRD contains — an $id is an identity, deliberately not a location, and nothing dereferences it.

That is not academic. evolith_tracker evaluates gate criteria against a flat field map: a criterion resolves a fieldPath and compares it. With no fields there is nothing to resolve, so a tenant can configure a rule over a document and nothing will ever read it — the gate checks that a file exists and never what it says.

Measured on a live tenant before this change:

Artifacts in the catalogue 34
Field schemas with any field 0
Criteria configured on any phase 0

What this does

Each registry entry now carries its fields, derived from the JSON Schema this repo already ships.

The schemas are not rewritten to a flat shape. They stay the source and this is a projection, so a schema change propagates on the next read instead of needing a second file kept in sync.

Across the corpus: 529 fields from 50 schemas. The PRD alone goes from nothing to 18, with types and requiredness:

metadata.identifier          text     required
metadata.status              enum     required   (Draft | In Review | Approved | Superseded)
executiveSummary.problemStatement    text  required
…

Three decisions worth reviewing

The type vocabulary is small on purposetext, rich-text, number, date, boolean, enum, url. It is exactly what the consumer's criterion operators can judge. A type outside it produces a field no criterion can evaluate, which is worse than a missing one: it can be selected in a rule and never satisfied.

Collections are omitted, and reported. gte, in-set and regex all assume a single value, so a list has no operator that can judge it. They are not dropped in silence: omittedFields says which and why, so someone counting 13 sections against 18 fields can see the difference is arrays and not a truncated schema. Corpus-wide that is 248 paths, all declared.

An unreadable schema costs one artifact, not the catalogue. A malformed file leaves that entry without fields rather than failing the whole registry, which every other artifact depends on.

Verification

  • 7 new unit tests over the derivation, including what it refuses to publish
  • Full core-api suite: 32 suites, 170 tests, green
  • Built into an image, deployed into a kind cluster, and read end to end by the satellite: GET /phases/discovery/artifacts returns the PRD with its 18 fields, and the Tracker's artifact-field-schemas/prd — which used to answer baseFields: [] — now resolves them

Ordering

The satellite side is a separate PR in evolith_tracker. This one should land first: with the Tracker's change deployed against an older Core, its artifacts simply have no fields again until this follows.

🤖 Generated with Claude Code

…has a shape

The registry names an artifact and points at its schema's $id. A consumer
therefore learns that a PRD is required in discovery and still cannot find out
what a PRD is supposed to contain: an $id is an identity, deliberately not a
location, and nothing dereferences it.

That gap is not academic. The satellite waiting on it evaluates gate criteria
against a flat field map, and with no fields a tenant can configure a rule over
a document that nothing will ever read — the gate ends up checking that a file
exists and never what it says. Measured on a live tenant before this: 34
catalogued artifacts, zero field schemas with any field, zero criteria.

So each registry entry now carries its FIELDS, derived from the schema the Core
already ships. The schemas are not rewritten flat: they stay the source and this
is a projection, so a schema change propagates on the next read instead of
needing a second file kept in sync. Across the corpus that is 529 fields from 50
schemas — the PRD alone goes from nothing to 18, with types and requiredness.

The type vocabulary is small on purpose: exactly what the existing criterion
operators can judge. A type outside it yields a field no criterion can evaluate,
which is worse than a missing one because it can be selected and never
satisfied. Collections are omitted for the same reason — gte, in-set and regex
all assume a single value — and REPORTED rather than dropped quietly, so someone
counting 13 sections against 18 fields can see the difference is arrays and not
a truncated schema.

An unreadable schema leaves that one artifact without fields instead of failing
the registry: one malformed file must not take down the catalogue every other
artifact needs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: aarroyo <beyondnet.peru@gmail.com>
@beyondnetPeru
beyondnetPeru requested a review from a team as a code owner August 23, 2026 15:11
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@github-actions

Copy link
Copy Markdown

📊 Bilingual Coverage Impact

PR Changes

  • Paired EN/ES files modified: 0
  • New EN files needing ES translation: 0

Repository Coverage

Metric Value
Total EN files 527
Total ES files 497
Paired files 0
Coverage 0%

Good: All EN changes have ES counterparts.


Generated by GitHub Actions

beyondnetPeru and others added 3 commits August 23, 2026 10:30
The labels these produce become the labels of a FORM — the satellite renders one
input per field — so the case matters. Title Case makes a form read like a menu
of commands rather than a set of questions, and it clashes with the sentence
case the rendering surfaces use everywhere else; two cases on one screen look
like two systems sharing it.

The acronym list exists because there is no rule to replace it. Lowercasing
every word turns `technicalFeasibilityId` into a label ending in "id", which
reads as a mistake, and leaving the camel case alone gives "Id", which reads as
a typo. Nothing in the spelling separates `id` from `is`, so the terms that get
shouted are named one by one. The list is short deliberately: a term missing
from it comes out as an ordinary word, which is merely plain, while a term
wrongly in it comes out shouting.

None of this runs for a schema that publishes a `title`. That is words chosen by
whoever owns the shape, and no amount of string-splitting here improves on them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: aarroyo <beyondnet.peru@gmail.com>
The labels this publishes become the labels of a FORM in whatever consumes them,
and until now every one of them was English. A Spanish reader got a Spanish
screen wrapped around English field names, which is not a partial translation —
it is the seam showing through at the exact place a person is being asked to
write something.

One language is computed and the other is written down, and the asymmetry is
real rather than arbitrary: these keys ARE English, so a label derived from
`cpuCoreLimit` is right by construction, while no amount of string-splitting
turns an English identifier into Spanish. Word order alone defeats it —
`technicalFeasibilityId` is «ID de viabilidad técnica», not three words in the
order they were written. So the Spanish has to come from somewhere, and this is
where.

It is keyed by LEAF NAME, not by field path. 559 published fields across the
corpus are only 348 distinct names, and keying by name is what makes `status`
read «Estado» in all nineteen schemas that declare it instead of nineteen
chances to say it differently. A schema whose context makes a shared word wrong
can override it with `x-title-es` on the property — the glossary is an addition
to what the schemas say, never a replacement.

Both languages travel TOGETHER on every field. A consumer syncs this catalogue
on a timer, tenant-agnostic and cached, and then renders it for whoever happens
to be looking; publishing one language per request would mean either a fetch per
reader or documents in the wrong language.

Nothing here can take the catalogue down. A missing or unreadable glossary costs
the Spanish labels and nothing else, and a field with no entry reaches a reader
with its English name — plain, not broken. The catalogue is what every gate
depends on, and no translation is worth failing it for.

The guard closes both directions, because a glossary rots two ways. A field
added with no entry half-translates a form, which nobody notices until a
customer does. An entry left behind by a rename looks exactly like coverage and
translates nothing. Neither is visible to any other check in this repository,
which is the whole reason this one exists — and its tests were written against a
version that only looked for the first kind.

Descriptions are still English. They are 252 sentences rather than 348 names,
they are prose rather than labels, and they belong in their own pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: aarroyo <beyondnet.peru@gmail.com>
The field names came out in Spanish and the headings above them did not, which
is the same half-translation one line higher up: «Límite de núcleos de CPU»
under «Technical constraints».

The section was being recovered downstream by splitting the path, and that is
the whole problem — splitting `technicalConstraints.cpuCoreLimit` yields
«Technical constraints» and can never yield «Restricciones técnicas». The words
belong to whoever owns the shape, headings included, so the enclosing object's
name now travels down with the fields it holds.

An object still is not a field: publishing the container as a field would offer
a path whose value is a document, which no operator can compare. It is a name a
person reads, which is a different thing, and it is carried as one.

The glossary needed the 96 container names, and the guard needed to start
demanding them. That second part is the point: without it the corpus could grow
a section nobody translated and the only symptom would be one English heading in
a Spanish form — which is exactly the kind of thing that ships. The guard's own
tests were updated to say so, including the one that used to assert a container
was NOT asked for.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: aarroyo <beyondnet.peru@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant