Imported from apollographql/graphos-factory (
skills/graphos-factory/SKILL.md). Install upstream withnpx skills add apollographql/graphos-factory --skill graphos-factory. Copyright stays with the author.
graphos-factory
You turn a REST API into an Apollo Connectors subgraph for the user's
GraphOS supergraph, and keep it maintainable. The subgraph is the
deliverable: a schema the user composes with their other subgraphs.
You are the generator: no tool writes the schema for you. The graphos-factory
binary (the shared references write graphos-factory-core <command>; here that is
graphos-factory, installed beside it as the same binary) and the shell wrappers under
graphos-factory-core/scripts/ do the mechanical work — reading a source into an inventory,
rendering placeholders, running rover, checking the invariants — and make no design
decisions at all.
Source kinds. workspace.yaml's source_kind names the source. Only
rest is built: a REST API wrapped with Apollo Connectors, with or without
an OpenAPI document. gRPC / protobuf, databases and existing GraphQL APIs are
planned, not built: if a user asks for one, say so plainly and stop — never
approximate one with the REST path. The workspace contract is
source-agnostic; each kind adds its own intake reference and key grammar.
Everything durable lives in the workspace, a git repo you create per
subgraph: inventory.json is what the source offers, selection.yaml is
what the user chose, <name>.graphql is the subgraph schema the engineer
edits, decisions.json (the calls, each with its alternatives, written only
via graphos-factory decisions), findings.json (the settled facts an instrument
reads, written only via graphos-factory findings) and memory.md are what you and
your predecessors learned. Read
workspace-contract.md once for the full
layout. The core's references are in graphos-factory-core/references/, this target's in references/.
Never regenerate the schema wholesale after the first commit. Compute the
delta and edit only what changed. Losing hand edits to a regeneration is the
specific failure this skill exists to avoid — and a hand edit you have not
codified (see "Hand edits" below) blocks an apply until you have.
This target's contract is the subgraph's place in the user's supergraph,
and most of it is not decided yet: the three references under references/
are placeholders that state the open questions. What holds today, enforced by
the binary: {{BASE_URL}} and {{AUTH_EXPR}} are the only placeholders, with
the scheme prefix outside the placeholder, and template.yaml gives each a
local test value (whether to keep them or author literals is open,
export-graphos.md); the Federation directives the schema may
import are @key, @shareable, @requires, @provides and @external
(federation-subgraph.md); one @source is the shape the
layers model, and a second is a lint warning (unmodelled, not forbidden), not
an error. No tag vocabulary is enforced for this target: unknown-tag
never fires, and a @tag means whatever the user's supergraph uses it for.
Before anything else
- If a workspace exists, read
.factory/decisions.json(graphos-factory-core decisions list ., including--openfor questions still awaiting the user),.factory/findings.json(graphos-factory-core findings list .) and.factory/memory.mdin full. Do not re-open aresolveddecision unless the user asks. When they want to revise an answer to the same question,graphos-factory-core decisions reopen . --id D-nnnnclears it and puts the record back toopen, then settle it again withdecisions resolve; when the question itself has changed, record a new decision instead. Never edit a record in place — the file is written only through that command. If the workspace still carries a legacy.factory/decisions.mdand nodecisions.json, rungraphos-factory-core decisions migrate .once to import it (it preserves theD-nnnnids and removes the.md), then continue. Adecisions.jsonfrom before ADR 0113 (provenance records titledHand edit codified:orSource patch:, prose with no alternative) is split once withgraphos-factory-core decisions migrate . --split: sort each record it lists by the test under "Write it down", then run it with--sorted(workspace-contract.md §decisions.json). - Read the target's contract (named above and under References) and
naming.md. Violating them breaks the deployed render rather than a test: the target's@sourceand placeholder rules, and{Service}_Type/{service}_fieldwith snake_case for multi-word names. - For new intake or before
select/apply, make the cheap call first: does the user want customer-/tenant-specific fields the spec does not define (analytics fields, CRM custom objects, CMS content types)? If not, it is a generic wrapper — recordcontext_mode: genericinworkspace.yamland move on (an unrecorded mode also reads as generic, so nothing blocks on it); amodelIdargument or a JSON field alone does not make it specialized, and nocontext.yamlis needed. Do not ask for a tenant model, metadata export or credential merely because the endpoint accepts an identifier or JSON. Only if it is specialized or you are unsure, readcustomer-context.md(under References) and assess there — recordcontext_modeand any dependencies before authoring. - Pick the verb the user needs (the target's verbs,
initamong them, follow this table) and read the reference it names.
Verbs
| Verb | Read first | What you do |
|---|---|---|
discover |
spec-intake or api-discovery | build/refresh inventory.json; assess customer context and update its records/snapshots within the authorized scope; report the diff; never edit schema or selection |
select |
naming, schema-authoring | agree operations, roots, names, field exclusions, tags, pagination; surface every [read?] POST and confirm with the user which are reads before giving one root: query; write selection.yaml (each included operation with include: true and its graphql root and name — nothing writes this file for you); then graphos-factory-core selection draft proposes a response.envelope per included operation — confirm each one with the user and set confirmed: true, because a draft nobody agreed to must never become the schema's shape; it also drafts one links: entry per candidate_entity_link fact (graphos-factory-core inventory links . lists them) — confirm each link with the user too (confirmed: true, or include: false with a reason), because a relationship field is one request per parent and only the authored schema knows whether it would be circular; record each design question as a decision — graphos-factory-core decisions add . --title T --question Q --choice id:label … [--multiple] — and settle each with the user via graphos-factory-core decisions resolve . --id D-nnnn --chosen id --note …, never as loose chat messages; record what the references settle for you as a finding (graphos-factory-core findings add . … --cites REF) when an instrument needs it, and otherwise do not record it; to revise a settled answer, graphos-factory-core decisions reopen . --id D-nnnn and resolve it again. |
apply |
schema-authoring, mapping-language, testing | graphos-factory-core lock --check (stop on uncodified hand edits; the provenance drift it lists is not a stop); graphos-factory-core context check (resolve build gaps before authoring); graphos-factory-core reconcile --baseline HEAD names the delta (on a first apply the schema is not in HEAD yet, and every span reads as added); edit the schema for exactly those spans, keeping every override's assertions true; for each confirmed link, graphos-factory-core links apply . --dry-run prints the field-level @connect keyed by $this (no @key; no header and no argument unless the by-id root field carries a per-call credential, which it mirrors — same argument, same header or query parameter; for a nullable fk, the isSuccess and selection null guards, because the router still sends GET …/ for a null parent) to paste into the host type — it refuses circular, self, no-fk-field, field-exists, nullable-fk, no-host, no-root-field and target-refused (a stale entry whose keep-or-drop decision is not resolved keep, ADR 0100, ADR 0113) (connectors-language § Relationship fields) and exits 1 whenever any entry is refused, even with others printed; graphos-factory-core scaffold drafts the case, stub and unit entry of every operation that has none (the notes say what the unit entry could not carry) — audit every placeholder, then e2e.sh --generate; run it again (clean; changed since HEAD ⊆ the delta); graphos-factory-core source-coverage . OP-KEY --check on every operation you touched, and graphos-factory-core source-coverage . --check (no OP-KEY: every selected operation, each failing one named) before lock — zero unaccounted and zero unresolved in both directions, and no unaccounted behaviour row (an optional argument's doc comment must carry the source's omission sentence, or a behaviour omit says why not — a finding's not-applicable, a decision's editorial, ADR 0095), is the bar (--check fails closed); each path you meant to leave off is recorded, not a silence: one you chose not to expose is a decision whose alternative is exposing it, graphos-factory-core decisions add . --title T --question Q --choice omit:L --choice expose:L --resolved --chosen omit --note WHY --omit 'op|direction|path|editorial', and one an expression consumes is a finding, graphos-factory-core findings add . --title T --body TEXT --cites REF --omit 'op|direction|path|consumed' (quote each and type plain pipes: each backslash here is only this table's escape; findings add refuses editorial); an unresolved row from the classifier's own grammar (a request ->map(…)->first, a request-side ->match, a chained method such as ->match->first) (or a ... spread that is not the disc->match(…) abstract-type form, ADR 0058) is recorded and reported as not verified by this layer, never cleared by editing inventory.json (schema-authoring.md § Coverage); graphos-factory-core lock; validate; commit apply: |
codify |
workspace-contract, mapping-language | a hand edit to the schema was detected: learn why from the engineer or the diff, then graphos-factory-core codify --key K --reason R with --expressed (after moving the intent into selection.yaml) or --assert kind=value…, and --context TEXT for the why the diff cannot show (codify records no decision); for an edit to a pinned spec, graphos-factory-core codify --source PATH --reason R, then, in a spec-backed workspace, rebuild the inventory (graphos-factory-core inventory build PATH; an intake: mixed workspace re-runs infer --update-inventory after it) so the correction and its patch marks reach inventory.json, and re-run reconcile; commit decide:. --key refuses a span that is in sync with the lock: a correction to something the tool inferred is not a hand edit — see the first policy below |
validate |
testing, customer-context | check context with --phase live before live requests; report live-only gaps as not_run without blocking offline layers; compose → unit → e2e → write-body proof → conformance → lint → json-accounting → live (the write-body proof and json-accounting layers are non-gating: a fail is printed as open findings, never as a pass; evidence/latest.json is contract_version: 2); write evidence/latest.json; report skipped layers as skipped; a unit run with zero cases is not_run, never a pass; every suite with zero cases fails unless its header's first sentence cites its decision (D-nnnn), and a skip: true case fails; every selected operation is a live case or a live exclusions: entry with its reason; lint's link-untested warns on each relationship field that no unit entry targets (<Type>.<field>) or no e2e case selects, and evidence's report names each one; link-null-untested warns when a nullable fk has no null-parent e2e case (a mapping for the empty-segment GET serving a case that selects the field), and, when tests/live.yaml exists, link-live-unaccounted when no live case selects the field and no exclusions: entry field: "<Type>.<field>" names it; a field with neither unit nor e2e, with no null-parent case, or excluded live is not validated; say so (Phase 7as (c)/(d) scaffold the tests; the hand-written route is connectors-language § Relationship fields); a conformance body the oracle could not judge is unchecked (no documented shape) or unmatched (no such operation, unreadable body — this one fails), never a pass; an accepted gap is graphos-factory-core codify --waive TARGET --status S --reason R; before reporting, graphos-factory-core lock --check --provenance; when it exits 3, relock (graphos-factory-core lock) only when a plain graphos-factory-core lock --check reports no changed or unattributed span, pinned-source problem or edited inventory (it then prints the relock advice, and --json sets relock_advisable) — the drift is provenance alone (a case, a stub, a memory.md line), or a document sources.lock.yaml no longer pins, which a relock stops watching on purpose. Otherwise the schema, a pinned document or inventory.json changed under the lock: stop and follow "Hand edits" (codify a span or pinned source, rebuild an edited inventory), because a relock would erase the only record of the edit |
record <op>… |
api-discovery, testing | graphos-factory-core probe each request (GET only unless told otherwise; scrubbed before it touches disk); graphos-factory-core infer --update-inventory for the oracle; graphos-factory-core fixtures for the stubs; note spec-vs-reality findings in memory.md |
This target's verbs, init first (it records the target, skill.name: graphos-factory; --target is optional):
| Verb | Read first | What you do |
|---|---|---|
init <name> |
workspace-contract | decide the subgraph name (snake_case or kebab-case, naming.md) and context_mode (step 3 above), fetch the description document yourself, then graphos-factory init <dir> --name N --spec FILE [--url U] --context-mode generic|specialized — it writes .factory/workspace.yaml (skill.name: graphos-factory), the working copy and its pinned vendor copy, sources.lock.yaml and inventory.json exactly as sources pin + inventory build would, and refuses a directory that already holds a workspace; --dry-run --json lists every file's path, content and sha256 without writing. It never runs git: standalone, git init the directory; already inside a repo (a monorepo, one folder per subgraph), let the enclosing repo track <name>/ and do not nest a second repo. Then commit. A no-spec workspace (intake: discovered) has no document to pin: write workspace.yaml (with skill.name: graphos-factory) and sources.lock.yaml by hand, as workspace-contract shows |
export |
export-graphos | not built yet: the subgraph is the deliverable; see references/export-graphos.md. Hand the user the validated workspace (the schema, template.yaml, the evidence) and say that publishing it to their graph is theirs to do; never run rover subgraph publish or open a PR to their schema repo on your own |
validate on this target also runs the target's evidence layer,
supergraph_check: composing the subgraph against the user's existing
supergraph. It is not built and always records not_run with its reason
(references/verification.md). The compose layer composes the subgraph alone.
Report supergraph_check as not run, never as a pass, and say plainly that
the subgraph has not been checked against the user's supergraph.
Instruments
One binary, the product's, plus the shell wrappers in graphos-factory-core/scripts/ around rover, Java
and curl for the validation layers. It is downloaded from the skill's GitHub releases (the rolling
edge pre-release until a versioned release exists) by the skill's own scripts/bootstrap.sh (the
SessionStart hook runs it), which also links it as graphos-factory-core; it is built from
crate/ only when developing the skill itself (bootstrap.sh --build). Every wrapper takes
the binary from $GRAPHOS_FACTORY_CORE_BIN, then that link in the bootstrap cache, then PATH, prints which
one and its version to stderr, and exits 78 (a fail, never a skip) when it is
older than the pin (crate/Cargo.toml, or $GRAPHOS_FACTORY_CORE_VERSION); evidence hands the
wrappers its own binary. On 78, re-run bootstrap.sh (--build in a checkout) or set
GRAPHOS_FACTORY_CORE_BIN — never lower the pin to get past it. Run everything from the workspace
root. graphos-factory-core <command> [verb] --help (or -h) prints that command's usage and reads
or writes nothing (ADR 0086). A flag the verb does not declare — a typo, or two flags
quoted into one argument — is refused with exit 2 and nothing runs (ADR 0097); fix the
spelling, never drop the flag. rover is needed for compose, unit and e2e; Java for
e2e's WireMock; nothing is ever installed into a workspace.
S=path/to/graphos-factory-core/scripts
export GRAPHOS_FACTORY_CORE_SCRIPTS=$S # evidence needs to find the wrappers
# If the host supplies a model identifier, set GRAPHOS_FACTORY_CORE_LLM_MODEL to that exact value.
# Never infer or guess a model identifier. The lock records unknown when this value is absent,
# and `lock` prints a one-line warning for that and for a skill source of binary-build (ADR 0090).
graphos-factory-core inventory build openapi.json # description document (OpenAPI 3.x or Swagger 2.0) -> .factory/inventory.json (refuses to overwrite a hand-edited one)
graphos-factory-core inventory list --tag Incidents # page the inventory, grouped by tag; 50 a page. --json: total, offset, limit, next_offset (null on the last page) — a loop over every operation follows next_offset (ADR 0091)
graphos-factory-core inventory list --support needs_review # what still needs a decision
graphos-factory-core inventory describe get:/v2/incidents # one operation, shapes expanded
graphos-factory-core inventory links . [--json] # every candidate_entity_link fact flat: shape, property path (album_id / []>album_id / songs[]>album_id), the canonical GET-by-id operation, whether the host is a list item and whether that operation is selected, then every GET-by-id refused as a target because its response does not carry its key — a hint, never a finding; judge each and record the call with `graphos-factory-core decisions add` (ADR 0069, ADR 0085)
graphos-factory-core inventory diff old.json new.json # what a spec refresh changed
graphos-factory-core context check . [--phase build|live] [--json] # declared customer requirements; exit 2 for missing context, 1 for invalid records
graphos-factory-core context capture . (--requirement ID|--input) --id ID --from FILE --representation raw|derived|transcribed … # copy one local non-secret artifact and record its provenance
graphos-factory-core decisions list . [--open] [--json] # the decision log (.factory/decisions.json): decisions only — calls with their alternatives, resolved, and open questions awaiting the user (ADR 0026, ADR 0113)
graphos-factory-core decisions add . --title T [--question Q] [--choice id:label]… [--multiple] [--phase P] [--affects PATH]… [--resolved --chosen id --note TEXT --decision TEXT --by user|agent] [--omit 'operation|direction|path|reason']… (a resolved request omit also removes an optional source body member from the write-body proof; a member the source requires stays a gap, `required_member_omitted`, and `decisions add` warns when an omit targets one) [--json-reason 'Type.field|reason']… [--null-handling 'operation|argument|behavior']… [--secret-field 'Type.field|disposition|reason']… # record a decision: an open question, or with --resolved a call already made (`resolution.by` defaults to agent; --note says why you did not ask); refuses, exit 1 `no-alternative`, a record that names its alternative none of three ways — a --question, two or more --choice, or an --omit with reason `editorial` — a settled fact is `findings add` (ADR 0113); --omit records a structured scope exclusion `source-coverage` reads, only on a resolved record, and an `editorial` omit lives only here; path `.` omits the whole direction for that operation, which is how to record an EmptyResponse 204's empty root row (ADR 0036); --json-reason records why a response field is left typed as the workspace's JSON scalar, from `crate::json_accounting::REASONS`, read only on a resolved record (ADR 0073) — never a selection.yaml field or a schema doc comment; --null-handling records what the connector sends for an explicit null argument (behavior: send_null or omit), read only on a resolved record (ADR 0079); --secret-field records a reviewed credential-shaped response field (disposition expose or exclude) that lint's secret-field-exposed rule reads, only on a resolved record (ADR 0078); direction `behaviour` waives an omission sentence an argument's doc comment leaves out, path `query:NAME`/`path:NAME`/`header:NAME`/`body:KEY.PATH`, reason `editorial` or `not-applicable` (ADR 0095)
graphos-factory-core decisions resolve . --id D-nnnn [--chosen id]… [--note TEXT] [--decision TEXT] [--by user|agent] [--force] # settle an open decision (with --force it replaces the whole resolution: pass the old --decision and the old note plus what you add, to amend a record without losing it); the same command a headless agent and the UI both use
graphos-factory-core decisions reopen . --id D-nnnn [--json] # revise a decision: clear a resolved/superseded record's resolution and set it back to open (everything else kept); refuses an unknown id, an open record, or a missing/invalid log (ADR 0060)
graphos-factory-core decisions supersede . --id D-nnnn [--json] # mark a resolved decision replaced: status only, its answer kept; it stops counting as resolved everywhere (its omits, json_reasons and null_handling, its affects, a links: decision: naming it), so re-record what still applies; the fix for lint's `stale-omit` (a finding's: `findings supersede`) (ADR 0103)
graphos-factory-core decisions migrate . [--keep-md] [--force] [--dry-run] [--json] # one-time: import a legacy .factory/decisions.md into decisions.json (preserves D-nnnn ids, parses fenced Omits: blocks into structured omits), then remove the .md
graphos-factory-core decisions migrate . --split [--sorted FILE] [--dry-run] [--json] # one-time, a pre-ADR-0113 log (FILE: YAML/JSON mapping each listed D-nnnn to `{as: decision, question, choices, chosen, by, note}` | `{as: finding, cites, body, …}` | `{as: memory, line}` | `{as: drop}`; without it the unassigned records are listed and nothing is written, exit 1 `unsorted`): codify records move onto the override, waiver or patch they shadow, refresh records become findings, a record with a question, choices or an editorial omit stays a decision; lists every other record for you to sort by hand and refuses to write without --sorted until each is assigned (ADR 0113)
graphos-factory-core findings list . [--json] # the findings log (.factory/findings.json): facts a reference, an ADR or the wire settled, F-nnnn, current until superseded; every omits/affects reader takes these with the resolved decisions (ADR 0113)
graphos-factory-core findings add . --title T --body TEXT [--cites REF] [--source agent|codify|sources] [--affects PATH]… [--omit 'operation|direction|path|reason']… [--evidence E]… [--related D-nnnn|F-nnnn]… # record a fact an instrument needs, or the next session does; omit reason `consumed` or `not-applicable` — refuses `editorial`, which is a decision
graphos-factory-core findings supersede . --id F-nnnn # a later finding replaces it; it stops counting
graphos-factory-core source-coverage . [OP-KEY] [--json] [--check] # source coverage: every fact the source offers is covered by the schema, or a resolved decision or current finding says why not (`spans obligations` is the old spelling); every request-body and response path the source offers (parameters are not classified), classified as mapped / consumed / omitted-decided / unaccounted / unresolved, plus unverified-default and transport-expansion-missing at an `x-expansion` boundary, plus a behaviour section when an optional argument's source says what omitting it does (documented / waived / unaccounted); zero of those four in both directions and zero unaccounted behaviour is the completion bar, and --check fails closed on any (ADR 0037, 0047, 0095); with no OP-KEY it checks every operation selection.yaml includes, one counts line each, --json wraps each operation's object in `operations` with `totals` and `failing`, an operation it cannot classify counts failing, and nothing selected exits 1 (ADR 0101); a resolved omit no row needs any more (the path is now mapped, envelope-read for an `editorial` entry, or no longer offered) is a `stale-omit`, counted and listed with its decision id but not part of the bar, and lint warns on it (ADR 0103)
graphos-factory-core spans json-accounting . [--json] [--check] # every response field the current SDL still types as the workspace's JSON scalar, nested and list-wrapped fields included, matched against a `json_reasons` entry on a resolved decisions.json record (never selection.yaml or a schema doc comment — Adam's rule) for a reason from a closed vocabulary (free-form-object, recursive, vendor-undocumented, polymorphic-without-discriminator, depth-cap) whose predicate is re-checked against inventory.json every run — accounted / unaccounted / stale (recorded reason no longer holds) / recoverable (inventory is now fully typed there, overrides the rest) / unresolved (reason recorded, but no shape located for the type: by name, root-field operation or parent property); `defaults.fields: all` is never itself a reason, and --check fails closed on anything but accounted (ADR 0073)
graphos-factory-core serialization . [--json] # read-only: the standalone CLI for the write_body_proof evidence layer; per body-sending write, each argument's value proven at its own body pointer / query key / path or header placeholder by an executed e2e case (needs the run log), omission at its pointer, explicit null per selection.yaml `defaults.null_handling` (state `omit`) or a per-argument null_handling decision; an argument the connector spells in a way the reader cannot place is placed by an executed case whose stub demands its value at one body pointer (`ambiguous_value` when the value is not distinctive; `--json` lists `placements`, static or executed); unproven_mapping and null_handling_undecided are open findings, never passes; replaces list-arg-unproven / mutation-cases / loose-write-body / unit-no-body (ADR 0079, Step 2)
graphos-factory-core reconcile . --baseline HEAD # schema <-> selection <-> inventory delta; overrides; hand edits; what moved since HEAD; exits 2 with the URLs to compare when not one connector pairs (a base-URL split, ADR 0074)
graphos-factory-core sources pin . --path openapi.json [--url URL] # keep the vendor's bytes as .factory/sources/*.upstream.*; record kind/version/hashes
graphos-factory-core sources status . [--json] # every pinned document: kind, upstream integrity, hand edits, patches; plus every recorded docs/probe source (url, retrieved_at, sha256, operations, note)
graphos-factory-core sources refresh . --path openapi.json --from FILE [--url URL] --reason R # a newer vendor document: re-apply patches[] (re-applied / obsolete / conflict), rebuild + diff the inventory; both shas and each patch's fate go to a finding (source: sources)
graphos-factory-core lock . [--check [--provenance]] [--skill-dir DIR] [--model MODEL] # record applied hashes plus skill, binary, toolchain, input and output provenance; --check also lists provenance drift, --provenance fails on it; a write warns on stderr when the model is unknown or the skill source is binary-build, naming --model / GRAPHOS_FACTORY_CORE_LLM_MODEL and --skill-dir / GRAPHOS_FACTORY_CORE_SCRIPTS (ADR 0090)
graphos-factory-core codify . --key K --reason R [--context TEXT] --assert contains=… # turn a hand edit into an override (reason, context, assertions), or --expressed (a finding only when --context is given); no decision record — --decision D-nnnn only attaches a real one (ADR 0113)
graphos-factory-core codify . --source openapi.json --reason R [--context TEXT] # turn a hand edit to a pinned spec into patches[] carrying reason and context; no decision record
graphos-factory-core probe . --key "get:/x/{id}/" --url URL # one recorded, scrubbed live request -> .factory/samples/
graphos-factory-core infer . --update-inventory # samples -> shapes (the no-spec conformance oracle)
graphos-factory-core fixtures . # recordings -> case-scoped WireMock stubs (.factory/recordings.yaml)
graphos-factory-core scaffold . [--op KEY] [--force] [--dry-run] # draft a case, a stub and a unit entry per selected operation from the connector + shapes (a sparse-fieldsets GET: an omitted and a `_fields_narrowed` case, no unit entry for a comma default); audit before --generate
graphos-factory-core scaffold . --op KEY --status CODE|all-missing # error cases: per documented non-2xx status a case with `# expect-upstream-status: CODE` and a stub answering it (the documented body, if any); refuses a stub equal to an existing one, or to another case planned in the same run (ADR 0077)
graphos-factory-core error-statuses . # one line per included operation: root field, documented non-2xx statuses, the statuses the last evidence run saw answer its own request, operation key (`-` for none); for e2e.sh, and to see what is still owed (ADR 0077)
graphos-factory-core selection draft . [--links|--envelopes] [--op KEY] [--force] [--dry-run] # propose a response.envelope per included operation and a links: entry per candidate_entity_link fact; every block lands `confirmed: false` for you to confirm (ADR 0069)
graphos-factory-core links apply . --dry-run [--link "<shape> > <path>"] [--json] # print the field-level @connect for each confirmed links: entry (the by-id selection verbatim, {$this.<fk>}, the one @source, no @key, the root connector's static headers and queryParams, and the by-id root field's per-call credential mirrored or none; a nullable fk gets the isSuccess and selection null guards, ADR 0084); refuses circular / self / no-fk-field / field-exists / nullable-fk / no-host / no-root-field / target-refused (a stale entry lint flags link-target-refused, until its keep-or-drop decision is resolved `keep`, ADR 0100, ADR 0113); never writes the schema (ADR 0069)
graphos-factory-core selection set . --op KEY… --include true|false [--dry-run] [--json] # toggle an operation's include flag in place, block or one-line flow entry, comments and anchors preserved (drives a UI toggle)
graphos-factory-core selection review . [--candidate FILE --expect-input TOKEN] [--expect-review TOKEN] # read-only JSON snapshot/review; never saves or applies
graphos-factory-core batch find . [--json] [--all-types] [--check] # per keyed type (graphql.entity), the operations returning an array of it and whether one takes a list of its key: batchable / partial-shape / needs-scope / style-conflict / paginated / list-no-key-filter / none (reads only: GET, an inventory read, or a selection root: query); with graphql.batch: true it prints a paste-ready type-level $batch connector (you paste it; nothing writes the schema); --check exits 1 when a batchable keyed type has no type-level $batch connector and no graphql.batch: false (lint batchable-entity-unbatched); 2 when the schema does not parse
graphos-factory-core scaffold . --op batch:<Shape> # for a type with a $batch connector: a case through a root whose selection maps the key alone, its root stub and an `x-required` lookup stub demanding the exact key list — e2e fails unless the router makes that request
bash $S/compose.sh . # rover supergraph compose (layer 1)
bash $S/unit.sh . [--only PATTERN] # rover connector test (layer 2)
bash $S/e2e.sh . [--generate] [--only PATTERN] # WireMock (java) + real Apollo Router; every stub loads once, for the whole suite — a stub's request matcher must be unique workspace-wide (layer 3); a case's `# expect-upstream-status: CODE` fails it unless the operation's own request was answered CODE (a status served to a nested lookup or side call is not the operation's), an error case without one on an operation documenting several statuses is UNPROVEN, never a pass, and a documented status no passing case saw answer its own request is `not_run` in `error_coverage` and leaves the operation `unchecked` (ADR 0077)
graphos-factory-core validate . # fixture/request bodies vs the spec (layer 4)
graphos-factory-core lint . # contract + coverage invariants, incl. fixture-collision (two stubs matching the identical request), sparse-fieldsets (a GET's `fields` default derived and forwarded; `sparse_fieldsets: {enabled: false}` opts out), and undocumented-root-field / argument-constraints-undocumented (a root field or constrained argument with no doc comment, or an optional argument whose doc comment drops the source's omission sentence, "If not passed, …" or "Defaults to …", traced through headers and nested write bodies too; the finding quotes the text to write), and argument-required-optional-in-source (an argument made `!` over an optional source parameter, error); secret-field-exposed (a response field named like a credential — token/secret/password/passcode/credential(s), or an api/private/client/secret/encryption/signing/access `*Key` compound; exclude it or `decisions add --secret-field 'Type.field|expose|reason'`, ADR 0078); decision-without-alternative (a decisions.json record with neither a question nor choices, warning, ADR 0113); entity rules for every @key type: entity-without-lookup (per resolvable key; `resolvable: false` stubs exempt) and entity-key-not-embedded (read through the selection) (errors), entity-without-consumer and entity-field-unresolved entity rules for every @key type: entity-without-lookup (per resolvable key; `resolvable: false` stubs exempt) and entity-key-not-embedded (read through the selection) (errors), entity-without-consumer and entity-field-unresolved (warnings); and two more warnings: unknown-tag (a `@tag` outside the target's vocabulary) and failure-case-missing (per operation, the documented non-2xx statuses no e2e case exercises, meaning the operation's own request was answered with it, not a nested one) (layer 5)
bash $S/live.sh . # tests/live.yaml against the real API (layer 6)
graphos-factory-core evidence . --scripts $S # run every layer, write .factory/evidence/latest.json (includes write_body_proof, in-process, same-run against wiremock_e2e's own current results — never a stale latest.json)
graphos-factory-core render . --out /tmp/x # placeholder-free copies, for debugging (+ router.yaml on this run's ports)
bash skills/<skill>/scripts/bootstrap.sh [--build] # install this skill's binary and its graphos-factory-core link (download; --build from crate/)
cargo test --manifest-path path/to/crate/Cargo.toml # the instruments' own tests
inventory list and describe exist so you never read a large raw spec:
page through the inventory and expand one operation at a time. A page holds
50 operations unless --limit says otherwise; operations alone is not the
whole inventory — keep calling with --offset <next_offset> until
next_offset is null.
This target's instruments:
graphos-factory init <dir> --name N --spec FILE [--url U] [--retrieved-at T] [--created-at T] [--context-mode generic|specialized|undecided] [--dry-run] [--json] # a new spec-backed workspace: workspace.yaml (skill.name graphos-factory), working + vendor copy, sources.lock.yaml, inventory.json; refuses an existing workspace; never runs git (ADR 0056)
The target adds no command of its own yet; every other instrument above is
the core's. One validated workspace shows the path end to end:
gitea (Swagger 2.0, pinned and patched;
every layer executed in CI against a seeded Gitea, the write included, and
supergraph_check recorded not_run).
Policies you apply every time (the why is in the references)
-
Query vs Mutation is semantic, and a POST is a write until the user says otherwise. The inventory records every POST as
writeand marks the ones whose name suggests a read with aread_hint([read?]ininventory list). Atselect, list every hinted POST and ask the user which are reads before writingroot: queryfor any;selection.yamlcarries the explicitgraphql.root, and a POST selected as a Query needs its reason recorded as a decision (the resolution of the open question you raised atselect, orgraphos-factory-core decisions add . --resolved …with itsqueryandmutationchoices): a write underQueryis one callers, caches and retries treat as safe, so the choice has to be reviewable. -
The inventory is facts; the selection is judgements.
inventory.jsonis a straight reading of the source: every field is checkable against the document, it is regenerable at any time, it is never hand-edited, and it never governs the schema. Every judgement that changes the schema or the tests — the GraphQL root, the field name, the response envelope, what is exposed of the pagination, tags, exclusions — is inselection.yaml, where the tool drafts it as a hint and you confirm it with the user. When an inference is wrong, do not correct the inventory (the lock hashes it;lintreportsunacknowledged-inventory-editandinventory buildrefuses to overwrite it): fix the document in the pinned working copy andcodify --source, or write the judgement you want intoselection.yamland re-runreconcile.codify --keyis for neither — it records a hand edit to the schema and refuses a span that is in sync. -
A relationship link is a judgement in
links:. The inventory'scandidate_entity_linkfact only proposes;selection draftwrites the entryconfirmed: false, and an unconfirmed draft never governs — no field is applied, reconcile notes it, lint warns. A confirmed link is a field-level@connectkeyed by$this, no@key, mirroring its by-id root connector's credential — none under@sourceauth (connectors-language.md § Relationship fields) — and, for a nullable fk, guarded so a null parent resolves to null instead of failing onGET …/(ADR 0084); the entity form is the cross-subgraph and$batchexception and needs a decision. A confirmed link whose target the current rules refuse or whose fact is gone is stale and raises an open decision for the field — choiceskeepanddrop, the refusal as its context; run thedecisions addcommand reconcile and lint print and name the record in the entry'sdecision:(ADR 0113). While it is open the link is reconcile drift,link-target-refusedand atarget-refusedrefusal fromlinks apply(ADR 0100). Resolvedkeepexempts it (ADR 0098); resolveddrop: setinclude: falseciting the decision and remove only that field and its hand-written tests on the next apply — never regenerate. -
Never an executable SDL default.
pageLimit: Int = 5is a value the router sends when the caller omits the argument and a consuming agent copies as "the value to use". WritepageLimit: Intand put the provider's default — with itsminimum,maximumandenum, whenever the inventory carries them — in the argument's doc comment (schema-authoring.md § Argument constraints). No offline layer sees the difference. The one exception is a sparse-fieldsets argument (fields: String = "…"), whose default lint derives and requires (§ Sparse fieldsets). The rest of an argument's doc comment is at most one source sentence, opt-in per service, and never its type or requiredness (§ Argument descriptions). -
Untyped beats unreachable, but typed beats untyped. Emit a
<Prefix>_JSONscalar only when the shape cannot be typed honestly (anyoneOfatconnect/v0.3; at v0.4 one whose members no wire value tells apart, or that is nested where no connector of its own can re-fetch it; free-form objects;additionalPropertiesmaps) and say why in a doc comment. At v0.4 a discriminatedoneOfis a union or interface, mapped as... kind->match([wire, { __typename: "Member", … }], …, [@, null])(mapping-language.md § Abstract types). Never a typed guess that drops payload data. -
An error's message path comes from the inventory's documented shape, never a guessed convention. Read
errors[].shape_reffor the operations a source's or connector'serrorsblock covers before writing the expression (schema-authoring.md § Errors);lint'serror-path-unresolvedchecks it. -
Every included operation gets executed evidence: a case plus a scoped fixture, a unit entry when its arguments are scalars, and a conformance check against the spec or the inferred schema. A skipped layer is reported as skipped, never as a pass.
-
Fixtures are truth. Never edit a recorded fixture to make a test pass — fix the schema, the case, or the expectation. Scrub before commit; a committed file names a credential's environment variable, never its value.
-
A
.factory/file is a regular file, or nothing. Every instrument reads and writes.factory/*through one custody module (ADR 0025): a symlink at any component of the path —.factoryitself,sources/, the file — is refused, on read and on write alike, and the message names the workspace-relative path, never the link's target.refused — a symlink, not a regular filemeans the workspace is wrong, not the tool: restore the real file (git checkout --it, or copy it in) and re-run. Never work around it by reading or writing the target yourself. -
Account for everything. Every operation the API has is
selected,excluded(with a reason),unsupported(with a reason), orunresolved. "What was left on the table" is the question the user is actually asking. -
Write it down — one test decides where: could a reasonable engineer have gone the other way, and would the service still be valid? (ADR 0113; the shapes are in workspace-contract.md)
- Yes →
graphos-factory-core decisions add, with the question and every alternative, resolved by the user or by you with--notesaying why you were confident enough not to ask. - No, because a reference, an ADR or the wire settles it →
graphos-factory-core findings add --cites …, and only when an instrument needs itsomitsoraffectsor the next session needs the fact. - A vendor quirk, a dead endpoint, an auth detail →
memory.md. Alsomemory.md, under## Tried and rejected: a negative result, an accepted residual no instrument can waive, a standing correction the user gave — none is derivable from current state, and the next session readsmemory.md, notgit log. - A measurement, a correction to an earlier record, a stash, "found and fixed" → the commit message, or an edit to the record it corrects.
- Something the next service would want →
graphos-factory-core/references/lessons.md.
A spec correction is none of these: edit the working copy, then
graphos-factory-core codify --sourcerecords it aspatches[]insources.lock.yamlwith the reason; add the live evidence asverified. - Yes →
- The subgraph is checked alone until
supergraph_checkis built. Compose, unit, e2e, conformance and live prove the subgraph by itself. Whether it composes with the user's other subgraphs (a shared type, a conflicting field, an entity another subgraph owns) is unverified: say so in every report, and record any entity or@shareablechoice you make as a decision so the next pass can check it (references/federation-subgraph.md).
Discover, then select, then apply
discover never edits the schema or selection. It rebuilds the inventory,
reports added / removed / changed operations, and assesses customer context.
It may persist requirements and authorized metadata snapshots; unresolved
inputs remain visible in context.yaml. The user changes selection.yaml
(by hand, through the UI, or by asking you); only then does apply touch
the schema, and only for the operations the delta names.
Hand edits: detect, codify, keep editable
.factory/applied.lock.yaml is the schema as you last wrote it, one hash
per span (header, each type, each root field by operation key), plus one for
each pinned document and one for inventory.json. Anything
that hashes differently is a hand edit — the engineer's or the UI's —
and graphos-factory-core reconcile, lock --check and lint all name it. An
edited inventory.json has nothing to codify: it is built, never edited, so
rebuild it and put the correction where it belongs (the pinned document, or
selection.yaml). Nor is a root field whose path several operations match
equally (unattributed-span): declare its operation in selection.yaml;
lock and codify refuse it (schema-authoring.md).
Never start an apply while one exists: you could not tell it from
your own delta, and "correcting" it back toward the selection is the exact
failure this skill exists to prevent.
When you find one, codify it before anything else:
- Learn why. Read the
git logof the file and the diff of the span; ask the engineer if the reason is not obvious. Never invent one. - Prefer the selection. If the edit is something
selection.yamlcan say — a tag, a rename, an exclusion, a root, a name — put it there, checkgraphos-factory-core reconcileshows no drift for the operation, thengraphos-factory-core codify --key K --reason R --expressed. The next apply reproduces the edit from the selection; nothing is frozen. - Otherwise assert the intent.
graphos-factory-core codify --key K --reason R --assert contains=… --assert tag=…records an override: what must stay true of the span, not its bytes. Write assertions for the point of the edit (the comment that explains it, the ordering, the extra argument), not for incidental text.--untilsays when to revisit;--expiresmakes reconcile say so.--pin(no assertions) freezes the span and is a last resort — lint warns on it. - Commit
decide:. codify wrote the why onto the override (reason, andcontextfrom--context TEXTfor what the diff cannot show) and refreshed the lock; it records no decision, and--decision D-nnnnonly attaches the entry to a real one. Add amemory.mdline when the reason is a vendor fact rather than a local preference.
An override does not lock an operation. When a later selection or inventory
change names an overridden span, edit it like any other, keep every
assertion true (reconcile checks them), say in the apply: commit which
intents you carried forward, and re-run graphos-factory-core lock. If a change
would make an assertion impossible, stop and ask — do not pick a side.
Read the override's reason and context before touching the span; that
is where the why lives.
The same rules govern a pinned spec. graphos-factory-core sources pin --path openapi.json keeps the vendor's bytes untouched as
.factory/sources/openapi.upstream.json and acknowledges the working copy
in applied.lock.yaml; from then on openapi.json is editable exactly like
the schema. When the spec is wrong (a field the API sends as null, a missing
response, a wrong type), edit the working copy — never the upstream — and
codify it: graphos-factory-core codify --source openapi.json --reason R
computes the structural difference from the upstream (formatting never
counts) and records it as patches[] on the entry, each operation with the
reason (and --context, when given). lock --check, reconcile and lint name an
uncodified spec edit (unacknowledged-source-edit) and refuse an apply
until it is codified; a modified upstream is an error to restore, never to
codify. A newly fetched vendor document is refreshed in with
sources refresh --path openapi.json --from FILE --reason R (the patches
are replayed and each reported re-applied, obsolete or in conflict; a
finding records both shas and each patch's fate; the inventory is rebuilt and diffed; see
spec-intake.md), never codified as patches and never copied over the
working copy; exit 3 means a conflict whose intent you must re-apply by hand
and codify, exit 4 an interrupted write to restore with the git checkout
it names. Every discover reads the working copy, so the inventory marks the
operations a patch touches. Add verified: (the sample or page that proves
the vendor wrong) to a patch by hand; a patch without it is a guess.
Finishing an apply
Report context readiness and any unresolved live requirements separately from connector test results. Then report, in this order:
graphos-factory-core lock --checkbefore you started (no hand edits);- operations added, changed, removed (the
graphos-factory-core reconcilereport before you edited, and itscleanline after, withchanged since HEADnaming only spans the delta covers); - every override: its assertions
ok, its drift from the selection (the engineer's, reported not fixed), and whether you changed it and why; - the validation table from
evidence/latest.json, naming every skipped or not-run layer and its reason; - the
graphos-factory-core lockresult, including the recorded skill revision, dirty state and commit hash.
The provenance record describes the authored state. The model value is self-reported. It does not prove the model identity or the full session history. It does not claim bit-for-bit replay.
Quote the passing tool output rather than describing it. If any selected operation lacks executed evidence, the workspace is not validated — say so plainly instead of listing the layers that did run.
When the user asks how the subgraph reaches their graph, answer from references/export-graphos.md: that step is not built, and its questions are open.
References
customer-context.md— read only when the intake is specialized or unclear (step 3 gates it): the specialized workflow, missing inputs, discovery, questions, and thecontext.yamlrequirements contractworkspace-contract.md— files, shapes, git conventionsconnectors-language.md—@source/@connect, expression variables, relationship fields, entities, errors, v0.3 vs v0.4mapping-language.md—selection/body/queryParams: shapes, the methods the router runs, literals, absence, troubleshootingnaming.md— prefixes, snake_case, the semantic root, aliasingschema-authoring.md— envelopes, pagination, argument constraints, scalar choice, enums, nullability, depth, the JSON policyspec-intake.md— reading a description document (OpenAPI 3.x or Swagger 2.0); whatsecuritySchemesandsecuritybecome in the inventory; spec pathology classes; pinned specs andpatches[]api-discovery.md— the no-spec playbook: docs, probes, provenance, confidence; recording an OAuth flow from the docstesting.md— the eight layers, what each is blind to, and the trapsselection-review.md— theselection reviewcontract a UI or a headless agent uses to propose a selection changelessons.md— cross-subgraph memory; append when you learn something vendor-independentfederation-subgraph.md— placeholder for the next pass: entities by default,@shareable/@external/@requires/@provides, relationship fields whose target lives in another subgraph, more than one sourceexport-graphos.md— placeholder for the next pass: how a finished subgraph reaches a GraphOS graph, the publication gate, placeholders versus literalsverification.md— placeholder for the next pass: the target's lint rules and thesupergraph_checkevidence layer (not_rununtil built)
