Skip to content

openDox + openXdox: two open-source layers, an installable app with users, projects and a database; domain descendants pin openXdox (Brett's ruling 2026-09-04) #656

Open

Description

@brettheap

Ruling — Brett Heap, 2026-09-04, in session (lane openxfactory-opendox), recorded verbatim

"we do not have a place to store projects. If i want to start a new project in a new repo, and make some specs, we have no good place to store my projects. I think we need to make this an app that installs and is hosted with a db. we should have users and projects and can expand the feature set."

"openDox is a dead project. it has almost empty repo. I think someone started with idea and stopped after a few days. lets use that name as the core opensource repo. we have two layers of opensource openDox and openXdox. The openXdox is openDox tuned for use with openXfactory. we will make openDox work to just manage documents and ideas. it will keep the integration with git and notebook lm etc and have all tools that help for document management and ideation. then openXdox will integrate with openXfactory."

Earlier in the same sitting: "We then further pin that down to medxDox and CodeXdox for use in those domain factories. If I install MedxFacotry, then I get a medXdox install running in the installed tenand with its own db."

What the ruling settles

  1. Two open-source layers, two repositories. opensoft/openDox — the neutral core: a hosted, installable app with its own database; users and projects; document and idea management; git and NotebookLM integration and the document-management/ideation tooling. opensoft/openXdox — openDox tuned for openxFactory: the corpus reader, doc-health and lifecycle integration, gates, the apply lane. Domain descendants (MedxDox, codexDox, …) pin openXdox in the <Domainx><Product> form; a domain-factory install brings its own descendant instance with its own database in the tenant.
  2. The name openDox is taken deliberately. docs/openxdox-naming.md (ratified 2026-08-13) rejected openDox as "taken"; Brett has ruled the existing use dead (a near-empty repo abandoned after days) and accepted the name for the core. The record owes an Amendment 3 carried by the change that creates the repository: it must state the collision facts as measured today — a GitHub organization opendox exists, and small repos named opendox exist including one active 2026 project in the adjacent agent-written-docs space — and Brett's acceptance of them.
  3. A departure from the wallet precedent, ruled. split-openxwallet-repo kept the integration seam inside openxFactory; here the integration layer is its own open-source repository (openXdox). The seam openxFactory keeps is the corpus itself and its governance; what moves to openXdox is the code that reads it.

Why this is filed as an issue

It is the governing object for the lane's claims (Rule 1) and the place the ruling lives outside a chat transcript. The engineering record it supersedes in part: openxdox-install-app-provisioning (staged 2026-08-14, gate met, exit unraised) and codexFactory #89 (standalone migration, open since 2026-08-25) — both fold into the topic this issue commissions.

What the lane does next

  • Brainstorm doc(s) under ideation/brainstorm/ capturing the two-layer topology, the app-with-database model (users, projects, documents, ideas), the tenant install story, and the openDox/openXdox boundary — the only place contradiction is legal.
  • A staging topic under ideation/staging/ (opendox-two-layer-product) with the settled claims from the ruling and the OPEN questions Brett has not yet ruled: what the database is authoritative for versus git; the runtime shape (the Hermes install's FastAPI + Postgres pattern is the in-house precedent); whether a domain install always brings its own instance and database or may point at a shared one; the corpus-adapter boundary that breaks today's two-way doc_health ↔ ideation_dashboard import cycle; sequencing against the five active ideation-dashboard changes (two with open archive gates); what of the current 80K-LOC dashboard belongs to openDox versus openXdox.
  • Then an OpenSpec change (contract/boundary changes go through OpenSpec before Speckit) — on the split-openxwallet-repo shape, adapted for a two-repo extraction that cannot be byte-identical.

Refs: docs/openxdox-naming.md, ideation/staging/openxdox-install-app-provisioning/, openspec/changes/archive/2026-08-28-split-openxwallet-repo/, codexFactory #89, the 2026-09-04 read-only review (lane openxfactory-smalls).

Lane: openxfactory-opendox

Activity

  1. brettheap commented on Sep 4, 2026

    @brettheap
    ContributorAuthor

    CLAIMED — lane openxfactory-opendox, session 01X7e6BQZysoULChFmDmgG1Z, 2026-09-04T15:19Z, for the brainstorm doc(s) + staging topic opendox-two-layer-product (ideation only; no contract, code or naming-record edit in this claim — those come with the OpenSpec change). Sibling check: no PR or branch carries opendox/two-layer; codexFactory #89 and staging topic openxdox-install-app-provisioning are the prior records and are folded, not duplicated. Stale after 4h with no PR.

    Lane: openxfactory-opendox

  2. brettheap commented on Sep 4, 2026

    @brettheap
    ContributorAuthor

    RULING Q1 — Brett Heap, 2026-09-04T15:24Z, in session (lane openxfactory-opendox), on the question "what does the openDox database own as the source of truth, versus git?": the database owns identity and coordination; git owns governed artifacts. Users, memberships, projects, the project-to-repository mapping, sessions and unsaved drafts live in the openDox database. Specs, changes, ideation documents and contracts stay in git, read from repositories and written back only through the apply lane. Every existing gate (doc-health, OpenSpec, the PR checks) therefore stays valid; the database is disposable relative to the corpus. Rejected: documents in the database with git as an export; and the hybrid (ideas in the DB until promoted).

    Lane: openxfactory-opendox

  3. brettheap commented on Sep 4, 2026

    @brettheap
    ContributorAuthor

    RULING Q2 — Brett Heap, 2026-09-04T15:31Z, in session (lane openxfactory-opendox), on the runtime shape: reuse the Hermes install pattern — FastAPI + Postgres, deployed the way xFactory-Hermes-Install is (live on AKS since 2026-07-19), OIDC through the Keycloak broker being adopted in QA; the OpsxFactory dox workload set (auth, dashboard, intent-inbox, token-minter) is the deployment shape it grows into. Rejected: bolting a database onto today's stdlib serve.py monolith; a new full-stack platform.

    Lane: openxfactory-opendox

  4. brettheap commented on Sep 4, 2026

    @brettheap
    ContributorAuthor

    RULING Q3 — Brett Heap, 2026-09-04T15:32Z, in session (lane openxfactory-opendox), on instance topology: one instance and one database per tenant, always. Every domain-factory install brings its own descendant instance (MedxDox, codexDox, …) and its own database inside the tenant, whether Opensoft operates it (the runbook's Case A) or the tenant does (Case B). No cross-tenant data ever shares a store. Rejected: a shared multi-tenant openDox with row-level isolation; a per-tenant default with a pooled option for operator-hosted tenants.

    Lane: openxfactory-opendox

  5. brettheap commented on Sep 4, 2026

    @brettheap
    ContributorAuthor

    RULING Q4 — Brett Heap, 2026-09-04T15:34Z, in session (lane openxfactory-opendox), on the seam: openDox defines a corpus-adapter interface; openXdox implements it. openDox declares how documents are listed, read, written back and checked, with no knowledge of OpenSpec or doc-health; openXdox implements that interface over openxFactory's corpus and check families. The two back-imports from scripts/doc_health/ into ideation_dashboard.boundary (derive_possibles.py, ideation_readiness.py) move into a small neutral module both sides depend on. The dependency points ONE way: openXdox depends on openDox, never the reverse. Rejected: openDox pinning doc-health as a library (inverts the layering); openXdox as a tuned copy with no shared interface (divergence, fixes land twice).

    Lane: openxfactory-opendox

  6. brettheap commented on Sep 4, 2026

    @brettheap
    ContributorAuthor

    DIRECTION Q5 — Brett Heap, 2026-09-04T15:48Z, in session (lane openxfactory-opendox), on the module split, verbatim:

    "we want to make openDox useful on its own, it shoudl be able to still manage docs and do brainstorming and connect to notebook lm. it is domain neutral and external from openXfactory. we need to make sure openXfactory brings in the core machinery to map to domains. we need to think how a patient managment and research maps to the openXdox. and how a finacial simulations or accounting questions would map in ledgerXfactory. same for marketing analysis in adXfactory. what is core to these that we pull out and put in openXdox. and what can pull up to openDox that does not rely on openXfactory. and what do we need to try to pull from openXdox to openDox to make openDox more useful as a braintorming and reserch analysis tool. a student could use openDox or a lab assistant. so we want that to still be useful on its own"

    What this settles (a THREE-layer test, not a two-way split):

    1. openDox must be useful alone to a student or a lab assistant: manage documents, brainstorm, research analysis, NotebookLM connection. Domain-neutral and external to openxFactory. The test for a module: would someone with no notion of factories, gates or tenants use it? Then it belongs here — and the brainstorm must actively look for what to PULL UP from today's dashboard into openDox to make it a better brainstorming and research-analysis tool.
    2. openXdox holds the core machinery openxFactory brings to map a domain onto the workbench — what is COMMON to how MedxFactory (patient management and research), LedgerxFactory (financial simulations, accounting questions) and AdxFactory (marketing analysis) would each map. The brainstorm must work those three mappings explicitly and extract the common core.
    3. Descendants (MedxDox, LedgerxDox, AdxDox, codexDox) hold the domain-specific mapping.

    The per-module assignment is therefore design work under this test, carried by the brainstorm set and the staging topic, not ruled here module by module.

    Lane: openxfactory-opendox

  7. brettheap commented on Sep 4, 2026

    @brettheap
    ContributorAuthor

    RULING C1 — Brett Heap, 2026-09-04T17:46Z, in session (lane openxfactory-opendox), on the recorded conflict "which openDox is dead": the collision is accepted knowingly; the repository is opensoft/openDox. Amendment 3 to docs/openxdox-naming.md records the measured facts — six GitHub repositories carry the name, and the GitHub organization opendox belongs to an unrelated Amazon-analytics project — and Brett's acceptance of them; no claim is made on the organization name; the brand lives under opensoft. Which specific abandoned repository prompted the word "dead" is immaterial to the record. Rejected: citing a specific dead repo as the basis; reconsidering the name (the openXnotes fallback stays documented, unused).

    Lane: openxfactory-opendox

  8. brettheap commented on Sep 4, 2026

    @brettheap
    ContributorAuthor

    RULING C2 — Brett Heap, 2026-09-04T17:47Z, in session (lane openxfactory-opendox), on the recorded conflict "what is openXdox": openXdox is the domain-mapping core, parameterized. It holds what every domain factory shares — typed artifact kinds, a governed lifecycle engine (statuses, gates, roles, evidence) and the dispatch/apply lane — parameterized by a domain profile that a descendant supplies. Engineering vocabulary ("requirement", "OpenSpec change", the doc-health check families) belongs to the engineering descendant codexDox, or stays in openxFactory as its own adapter over the corpus-adapter interface; a clinician using MedxDox never sees the word "requirement". This resolves the founding words "openDox tuned for openxFactory" as: tuned for the xFactory FAMILY's way of mapping domains, not for openxFactory's engineering corpus specifically. Rejected: openXdox as today's dashboard minus the pull-ups (every descendant would inherit engineering vocabulary); one repo with two packages (a boundary that is only a package line).

    Lane: openxfactory-opendox

  9. brettheap commented on Sep 4, 2026

    @brettheap
    ContributorAuthor

    RULING C3 — Brett Heap, 2026-09-04T17:48Z, in session (lane openxfactory-opendox), on the recorded conflict "the standalone student under Q1": standalone openDox creates and manages a plain local git repository per project. Documents are always git-backed; commits are the write path; a remote can be attached later. Q1 holds unchanged (the database never holds documents), and moving a student or lab-assistant project into a governed factory is a push, not a migration. Rejected: a loose-documents mode with the database holding content until a repository is attached; requiring a repository before the first save.

    Lane: openxfactory-opendox

  10. brettheap commented on Sep 4, 2026

    @brettheap
    ContributorAuthor

    RULING Q6 — Brett Heap, 2026-09-04T17:49Z, in session (lane openxfactory-opendox), on sequencing against the five active ideation-dashboard changes: freeze the dashboard now and carve immediately. The five active changes (add-composed-view-authoring, add-doxchat-model-intake, add-lens-document-selection, add-nightly-dashboard-refresh, retire-doxbench-chat-turn-v1) stop where they stand in openxFactory; their live deltas and open tasks (20 across the two with open archive gates) are RE-HOMED into the extraction change and the new repositories as part of the carve. No new dashboard change opens in openxFactory. The lane's recommendation (let the five finish in place, carve after the two gated ones archive) was put and NOT taken; the cost accepted is re-homing 20 open tasks and the never-green nightly-refresh lane mid-flight.

    Lane: openxfactory-opendox

  11. brettheap commented on Sep 4, 2026

    @brettheap
    ContributorAuthor

    RULING Q7 — Brett Heap, 2026-09-04T17:51Z, in session (lane openxfactory-opendox), on ownership, visibility and license: opensoft owns both repositories; both are PUBLIC from day one under Apache-2.0, matching the three public product repos (openChart, openPractice, openRepoShape). Descendants (MedxDox, codexDox, …) follow their domain repositories' visibility. Rejected: openXdox private until the descendants prove the boundary; MIT or AGPL-3.0.

    All questions the staging topic carried are now ruled: Q1–Q7 and conflicts C1–C3. The topic's exit path (an OpenSpec change on the split-openxwallet-repo shape, adapted to a two-repo, non-byte-identical extraction that also re-homes the five active ideation-dashboard changes per Q6) is unblocked.

    CLAIMED — lane openxfactory-opendox, session 01TF54UA1RBYrVC8E677z6zm, 2026-09-04T17:51Z, for (a) recording these dispositions in the staging fragment + INDEX row (doc slice, now) and (b) authoring the OpenSpec change that exits the topic (proposal/design/tasks/deltas; Amendment 3 to the naming record rides with it). No repository is created and no code moves in (a) or (b); creation and the carve are that change's realization after ratification. Stale after 4h with no PR.

    Lane: openxfactory-opendox

  12. 1044 remaining items

  13. brettheap commented on Oct 9, 2026

    @brettheap
    ContributorAuthor

    Lane: openxfactory-4 (openXfactory-4-openDox_extraction)

    HOLDER RULINGS — lane openxfactory-4, plan 038, 2026-10-09T01:10:20Z: lane 3's REVIEW-W1 T041 NAMES points (b) to (e), at f9cc4b0c (lane3-to-lane4.log, 01:09:45Z)

    • (b) check_finding admits any category name for a pathless finding. CLOSE the set. A pathless finding's identity.category must be an engine category, identity-collision or uncited-disappearance's original kind under option (D) (6069024023 item 2), which is itself one of those. Packs never raise pathless findings: T045 refuses an empty-path pack finding (6072086385 item 3).
    • (c) A pathless entry is bounded at 200 characters, not 40. ACCEPTED as written. A per-entry category's entry is also its pack_id, so the 40-character name rule already binds it through make_finding. Only entry-refused carries a raw, possibly malformed entry, and 200 is its bound.
    • (d) ENGINE_FIELDS mixes the ignored id with the refused fields. SPLIT it by behaviour into two named sets, each with its rule in the docstring: a pack's id is ignored, and pack_id, pack_version and baseline_class are the engine's. Behaviour does not change; this is a clarity fix.
    • (e) no-sandbox accepts an entry, and per-entry raising is unruled. no-sandbox takes entry "", and a non-empty entry is REFUSED, as for manifest-refused and dispositions-refused (lane 3's MINOR at health_contract.py:865). A run raises ONE such finding, never one per entry: data-model.md:382 says "ONE install-level finding … says why" (R2Q16 (a)). Only the per-entry categories and entry-refused take a non-empty entry.

    T041 applies (b), (d) and (e) with lane 3's two MINORs and two NITs in its next push, before DRAFT-CLEAN, each with a test row and a killed mutant where it adds a guard.

  14. brettheap commented on Oct 9, 2026

    @brettheap
    ContributorAuthor

    Lane: openxfactory-4 (openXfactory-4-openDox_extraction)

    CLAIMED — lane openxfactory-4, plan 038 T002e (the fifth bookkeeping batch), opensoft/openxFactory, 2026-10-09T02:18:51Z

  15. brettheap commented on Oct 9, 2026

    @brettheap
    ContributorAuthor

    Lane: openxfactory-4 (openXfactory-4-openDox_extraction)

    HOLDER RULING — lane openxfactory-4, plan 038, 2026-10-09T02:34:27Z: the pathless entry rule, refining 6072197564 (e) on lane 3's REVIEW-W1 T041 MINOR at health_contract.py:693 (c620dacd)

    A pathless identity's entry is fixed by its category, exactly as the engine builds it:

    • Per-entry categories (ENTRY_CATEGORIES: fetch-failed, digest-mismatch, declaration-refused, pack-crashed, pack-timed-out, pack-bound-hit, pack-output-refused) REQUIRE a non-empty entry, the valid entry's id (6072086385 item 1).
    • Install categories (no-sandbox, manifest-refused, dispositions-refused) and identity-collision REQUIRE entry "". disappearance_identity() always gives a collision original the entry "".
    • entry-refused takes either: the refused entry as written, bounded at 200 characters, or "" when it has none.

    check_finding refuses any other form, so it admits exactly what engine_finding builds. T041 adds one test row per case, before its push.

  16. brettheap commented on Oct 9, 2026

    @brettheap
    ContributorAuthor

    Lane: openxfactory-4 (openXfactory-4-openDox_extraction)

    RECORD — Brett Heap's word on #99 (T042), by interactive multiple choice, 2026-10-09T02:44:18Z

    Brett said "land #99 when green". The holder then asked how that fits his earlier word 6035546708 ("Write now, land after T027") and plan decision N-6 (a), and Brett chose "Green now, land after T027 (Recommended)":

    Not chosen: landing #99 ahead of T027, or landing it while T017 and T027 exclude it by commit.

  17. brettheap commented on Oct 9, 2026

    @brettheap
    ContributorAuthor

    Lane: openxfactory-4 (openXfactory-4-openDox_extraction)

    RECORD — Brett Heap's word on #97 (T041), 2026-10-09T02:46:40Z

    Brett said "land #97 when green". #97 is T041, in phase 5's wave 1, under the same order as #99 (6035546708, N-6 (a)). The holder applies Brett's choice for #99 (6073187085, "Green now, land after T027 (Recommended)") to #97 likewise:

    • T041 finishes every gate now: lane 3's REVIEW-W1 MINOR at health_contract.py:693 (ruled in 6073087924), the full suite, CI and SonarCloud, Copilot and Codex at the head, 0 threads, and the live body.
    • It reports DRAFT-CLEAN and stays a frozen DRAFT.
    • After T027 lands, Archive add-register-edit-lane: first unattended fulfilment observed #97 merges main and gets new reviews at its final head, and the holder lands it.
    • T041's DRAFT-CLEAN still launches T044 and lane 3's pack phase B.

    If Brett wants #97 ahead of T027, his word changes this.

  18. brettheap commented on Oct 9, 2026

    @brettheap
    ContributorAuthor

    Lane: openxfactory-4 (openXfactory-4-openDox_extraction)

    HOLDER RULING — lane openxfactory-4, plan 038, 2026-10-09T13:28:47Z: lane openxfactory-4's PR opensoft/openDox-code#99 (T042), Copilot's "previously missed" item at fbf9e77e (review 5465167781): a cyclic value makes _carries_nul (src/opendox/runtime/health_store.py:149) raise RecursionError instead of the store's RefusedError

    FIX-NOW under 5988818366, not an accepted limit. The walk regressed a refusal the store already had.

    • Before the walk, the store refused a cycle by name. json.dumps, with its default check_circular, raises ValueError: Circular reference detected, and _json_text turns that into RefusedError.
    • The walk broke that. _refuse_nul, which 6072086385 item 4 put in front of the dump, recurses without bound. A one-line payload (d = {}; d["k"] = d) now escapes as RecursionError, which is not the store's documented refusal.
    • It still fails closed: no statement runs, nothing is written, and the message quotes nothing, so this is not a bypass. But the payload is simple, the fix is small, and D18: project edits queue instead of one-in-flight refusals #99 is re-reviewed at its final head after T027 anyway (6073187085).

    The required behaviour. RecursionError never escapes the store. A cyclic value in any field the store accepts (identity, evidence, locator, pack_pins, sandbox) is a RefusedError that names the field, raised before any statement and quoting nothing. So is a value nested deeper than the walk can follow. T042 chooses the means: a cycle-safe walk, the dump before the walk, or translating RecursionError at that one shared boundary.

    Gates, before T042's DRAFT-CLEAN:

    • Tests: a cyclic mapping, a cyclic list and an over-deep nesting, each refused by name with no row written.
    • The mutant that removes the guard is killed.
    • The full suite reruns at the new head.
    • Copilot and the Codex trigger at that head.
    • The PR stays a frozen DRAFT and lands after T027 (6073187085).

    The body records the fix, citing this comment. It does not go under Stated limits.

  19. brettheap commented on Oct 9, 2026

    @brettheap
    ContributorAuthor

    Lane: openxfactory-4 (openXfactory-4-openDox_extraction)

    HOLDER RULING — lane openxfactory-4, plan 038, 2026-10-09T13:40:55Z: lane 3's REVIEW-W1 T041 (EARLY) MINOR at src/opendox/health_contract.py:706-707 (_entry_breaks_form, the per-entry branch), at #97 4cc065ad

    FIX (lane 3's first option). 6073087924 said check_finding admits exactly what engine_finding builds. That stands as the rule, and the code is brought to it:

    • The check. For the per-entry categories (ENTRY_CATEGORIES), check_finding refuses an entry that is empty, the reserved opendox (contracts/health-packs-manifest.md:24), or not a 1-to-40-character name (the pack-id name rule). One test row each.
    • The probe. Lane 3's exactness probe found 21 mismatches; after the fix it must read 0. Quote it, and kill the mutant that drops each new refusal.
    • Ruling (c)'s premise, corrected. 6072197564 (c) assumed the 40-character rule binds through make_finding. It does not for a re-raise, whose pack_id is opendox. For a per-entry category's entry, the rule binds through this check.

    T041 applies it before its DRAFT-CLEAN. PHASE B branches from the head that line names.

  20. brettheap commented on Oct 9, 2026

    @brettheap
    ContributorAuthor

    Lane: openxfactory-4 (openXfactory-4-openDox_extraction)

    HOLDER RULINGS — lane openxfactory-4, plan 038, 2026-10-09T17:40:36Z: four items owed since the 13:45Z pause

    1. refused-patch joins ENGINE_KINDS (lane 3's 13:40:37Z ask, from T049's prep at #97 4cc065ad). T041 adds it (T049's recommendation).

    • Why: a refused patch is "a finding against the pack naming refused_patch and reason" (T049's entry; data-model.md :209, :223, :369), and only the engine raises it. Outside ENGINE_KINDS, T045 would let a pack declare the kind and forge engine-style refusals.
    • The form: it is a PATHED engine kind, like uncited-disappearance, not a category. So it stays out of CATEGORIES and the pathless set (6072197564 (b)).
    • Tests: T041 adds one test row. T045's refusal of a declaration naming it is T045's own test.

    2. T015's two limits (lane 3's R1-T015 EARLY line at #96 e7ee97b8). Both are ACCEPTED LIMITS under 5988818366, stated in #96's Stated limits. No push.

    • (a) ssh://token@host and the scp-like token@host:path keep the token. That slot is the ssh login name, which redact_destination spares by design, as the neutral port does. Redacting it would erase the routine git@host display, and a token there is the host port's own misconfiguration (operator-only).
    • (b) A raw token in a host port's prose, with no URL, passes. This is the inherent limit of pattern redaction. T015's guarantee is that a refusal is redacted as strictly as the report, and neither covers a bare secret in prose. Known-prefix redaction is a possible later hardening, outside release 2.

    3. path and U+0000 (Codex P1 4230671177 on #1283).

    • Already true: 6072086385 item 4's principle, "every string a finding carries", covers path. T041 already refuses it: _path refuses C0 and C1 controls and lone surrogates, as the pinned schema does.
    • (a) The contract: contracts/health-finding.md's path row and its bounds state that bound. T002e encodes it.
    • (b) T041: adds a test row for U+0000 in path if it lacks one.
    • (c) T042: the store refuses U+0000 in EVERY text parameter it binds (path among them), as RefusedError naming the column, before any statement, never a driver DataError. This is the same defence in depth as _json_text. One test row (U+0000 in path) and its mutant.

    4. entry-refused's raw entry versus the pinned schema (Copilot 4230698859 on #1283). The pinned schema wins. "The refused entry as written" (6072197564 (c), 6073087924) holds only where the entry conforms. identity.entry for entry-refused is:

    • "" when the entry has no id;
    • the id as written, when it is a string matching [a-z0-9-]+ of at most 200 characters;
    • otherwise, sha256- followed by the SHA-256 hex of the id's canonical text. This covers a malformed or over-long id, a non-string id, and one carrying U+0000. It is never truncated and never a schema exception.

    This is 6069507373 item 7's rule (a string over the bound becomes its SHA-256 hex) in the only form a closed {category, entry} identity allows.

    • T041 adds the helper that builds it. It is deterministic, total over anything the manifest parser yields, distinct for distinct ids, and never raises. T041 documents the canonical text in the contract, with test rows for an absent id, a conforming id, an uppercase or spaced id, an id with U+0000, a non-string id and a 201-character id. check_finding already admits the form.
    • T002e encodes it in T041's note and in the contract's entry-refused row.
  21. brettheap commented on Oct 9, 2026

    @brettheap
    ContributorAuthor

    Lane: openxfactory-4 (openXfactory-4-openDox_extraction)

    HOLDER RULING — lane openxfactory-4, plan 038, 2026-10-09T20:12:58Z: item 5, the exact canonical text behind entry-refused's digest form (6086098003 item 4). Copilot thread 4232974858 on #1283 at 1d413693 asks for it in the contract now. It is ruled here, so T002e encodes it and T041 builds it to the letter.

    • The canonical text of a refused entry's id is its JSON text with sorted keys, no whitespace and ASCII-only escapes: Python's json.dumps(id, sort_keys=True, separators=(",", ":"), ensure_ascii=True, allow_nan=True, default=str).
      • So every value the manifest's YAML parser yields has one spelling.
      • A value JSON cannot represent (a YAML date, timestamp or binary) is spelled by its str().
      • If that call raises (a mapping whose keys JSON cannot spell or cannot sort, or a self-referencing value), the canonical text is the id's repr(), so the rule is total and never raises.
    • identity.entry is sha256- followed by the lowercase hexadecimal SHA-256 of that text's ASCII bytes: 71 characters, inside the schema's [a-z0-9-] pattern and the 200-character bound.
    • Unchanged from item 4: "" for an entry with no id, and the id as written when it is a string matching [a-z0-9-]+ of at most 200 characters.
    • T041's tests add a row for a mapping id with mixed-type keys (the repr() fallback), beside item 4's rows.
  22. brettheap commented on Oct 9, 2026

    @brettheap
    ContributorAuthor

    Lane: openxfactory-4 (openXfactory-4-openDox_extraction)

    HOLDER RULING — lane openxfactory-4, plan 038, 2026-10-09T20:29:32Z: item 6, Copilot's three items on #1283 at 163ed13c about 6088484643

    • (a) Thread 4234200786 (date versus string): ACCEPTED LIMIT under 5988818366.
      • The collision: with default=str, a YAML date or timestamp id digests like a malformed string id that spells its str(). A conforming string such as 2026-10-09 is kept as written, so it never collides.
      • Why it is accepted: a collision needs two refused entries in one manifest, one a crafted non-JSON YAML value and one its spelling, which makes it operator-only and exotic. It is never silent: two findings with one identity raise identity-collision.
      • Where it is stated: the contract's entry-refused row.
    • (b) Thread 4234200849 (a non-ASCII repr()): FIX. The fallback is ascii(id), Python's repr() with every non-ASCII character escaped, so the canonical text is always ASCII and the digest never raises.
      • T041: changes its helper and adds a test row with a non-ASCII key in a mixed-type mapping.
      • T002e: encodes ascii(id) in place of repr(id).
    • (c) Copilot's "previously missed" item, 6082100803 not encoded: ENCODE it in the same push. For ENTRY_CATEGORIES, check_finding refuses an entry that is empty, the reserved opendox, or not a 1-to-40-character name. It corrects 6072197564 (c)'s premise, because a re-raise's pack_id is opendox.
  23. brettheap commented on Oct 9, 2026

    @brettheap
    ContributorAuthor

    Lane: openxfactory-4 (openXfactory-4-openDox_extraction)

    HOLDER RULING — lane openxfactory-4, plan 038, 2026-10-09T21:20:15Z: item 7, Copilot thread 4234624749 on #97 at 471b7231 (T041): non-string mapping keys in refused_entry()'s canonical text

    ACCEPTED LIMIT under 5988818366, the same class as 6088732352 (a). No code change.

    • The collision: json.dumps spells a non-string mapping key in its JSON form, so {1: "a"} and {"1": "a"} share a digest, and so do True/"true" and None/"null".
    • Why it is accepted:
      • Like the date-versus-string case, it needs two crafted refused entries with mapping ids in one manifest: operator-only and exotic.
      • It is never silent, because two findings with one identity raise identity-collision.
      • Making the text injective across YAML value types would need a typed encoding of every value at every depth, which is out of proportion to a refused entry's identity.
    • The contract's entry-refused limit is widened (T002e encodes it with item 6): the canonical text is not injective across YAML value types. A date or timestamp and its str(), and a non-string mapping key and its JSON spelling, can share a digest. identity-collision reports any such collision.
    • T041: resolves the thread citing this comment. DRAFT-CLEAN proceeds at 471b7231 once its suite exits 0 and validate is green. Copilot's "Changes recommended" at that head stands only for this ruled limit, and Archive add-register-edit-lane: first unattended fulfilment observed #97 is re-reviewed at its final head after T027.
  24. brettheap commented on Oct 9, 2026

    @brettheap
    ContributorAuthor

    Lane: openxfactory-4 (openXfactory-4-openDox_extraction)

    HOLDER RULING — lane openxfactory-4, plan 038, 2026-10-09T22:45:14Z: item 8, lane 3's REVIEW-W1 T041 (EARLY) NO-GO at #97 471b7231 (health_contract.py:526-532, refused_entry()), refining 6088484643 and 6088732352 (b)

    (a) MAJOR: FIX, as lane 3 proposes.

    • The defect: an id that yaml.safe_load yields as an integer past Python's 4300-digit limit (id: 0x plus 4000 fs, a long 0b literal, the same nested in a list, or inside a mixed-key mapping) makes json.dumps raise ValueError, and the ascii() fallback raises it again. That breaks "never raises".
    • The fix: the second fallback catches ValueError as well as RecursionError. An id that neither spelling can write takes ONE fixed text, <past the int-digit or recursion limit>, which replaces the recursion-only text.
    • Collisions: two such ids share a digest, an ACCEPTED LIMIT that identity-collision reports.
    • Tests: one row each for the four ids lane 3 reproduced, each returning a sha256- entry and never raising.

    (b) MINOR: FIX.

    • The defect: default=str spells a YAML !!set id by its str(), which follows hash order, so the digest varies with PYTHONHASHSEED, against "deterministic".
    • The fix: the default hook spells a set or frozenset as the list of its members sorted by each member's ascii() text. Everything else it meets is still spelled by str().
    • Limit: a set reached only through the ascii() fallback (inside a mapping JSON cannot spell) keeps Python's order. That is an ACCEPTED LIMIT: at worst that refused entry's finding is raised again each run.
    • Tests: one row that digests one set id under at least two PYTHONHASHSEED values, in subprocesses, and gets one entry.

    Then: T041 applies (a) and (b), red first, with their mutants, and pushes. Its DRAFT-CLEAN waits for lane 3's delta at the new head. T002e encodes item 8 in the contract's entry-refused row (the fixed text, the set clause and both limits).

    Carried forward for T046 and T057, from lane 3's same line: T041 prints nothing, and pack strings pass through verbatim, with only NUL and surrogates refused. So T046's CLI and list --json, and T057's HTTP and view, must escape at their print boundary under one trust model, swept with the fixed credential corpus.

  25. brettheap commented on Oct 10, 2026

    @brettheap
    ContributorAuthor

    Lane: openxfactory-4 (openXfactory-4-openDox_extraction)

    HOLDER RULING — lane openxfactory-4, plan 038, 2026-10-10T23:32Z: item 9, lane 3's REVIEW-W1 T041 (EARLY) GO at #97 f924db0d with 2 NITs, refining item 8 (6090537166)

    NIT 1: FIX, in T041's after-T027 round.

    • The gap: a mutant that sorts a set's members by repr() instead of ascii() survives (tests/test_health_contract.py:1394-1395, code at src/opendox/health_contract.py:502), because the only set row has ASCII-only members. For the members e-acute and f, repr() sorts f first and ascii() sorts e-acute first, so the digests differ.
    • The fix: one !!set row with a non-ASCII member that pins the ruled ascii() order, killing that mutant.
    • When: in the round where Archive add-register-edit-lane: first unattended fulfilment observed #97 merges main after T027 and gets new reviews at its final head (6073211034). It does not hold DRAFT-CLEAN at f924db0d, since lane 3 rated it non-blocking. Lane 3's FORMAL GO at f924db0d stands, and its delta at Archive add-register-edit-lane: first unattended fulfilment observed #97's final head covers the row.

    NIT 2: ACCEPTED LIMIT, with no code change.

    • The case: the 4300-digit limit is an interpreter setting, not a constant. Under PYTHONINTMAXSTRDIGITS=640, an id of 0x plus 3000 fs takes the fixed text <past the int-digit or recursion limit>, while at the default it is spelled out, so its digest differs between the two interpreters.
    • Why accepted: it is operator-only, since it needs the same pack checked under two interpreter settings. At worst that refused entry's finding is raised again under the other setting, the same class as item 8 (b)'s limit (convergence rule 5988818366).
    • Then: T002e states it in the contract's entry-refused row, beside item 8's two limits. T041's body names it under its stated limits.

    Also from lane 3's line: the CI failure on #97 at 471b7231, tests/test_console_token_delivery.py::test_a_server_outlives_no_killed_run, is not from plan 038's T041. The file is byte-identical on #97, on main c94878be and at the merge base. It came in with main's 32943cb (#84) and is a race in the test: it reads /proc/<pid>/stat but catches only FileNotFoundError, so a child reaped mid-read raises ProcessLookupError. The fix (catch both) belongs on main as a separate follow-on, not in #97. Until then, a red from this node is re-run with this cause cited.

  26. brettheap commented on Oct 11, 2026

    @brettheap
    ContributorAuthor

    Lane: openxfactory-4 (openXfactory-4-openDox_extraction)

    HOLDER RULING — lane openxfactory-4, plan 038, 2026-10-11T00:5xZ: item 10, Copilot's two "previously missed" points on T016's #100 at 99c4e079 (review of 2026-10-10T23:30:21Z), which T016 proposed as accepted limits

    Ruled: FIX, in T016's current round (the round that fixes lane 3's MAJOR), not an accepted limit.

    • The defect: on a governed repository with a host instrument, landing_refusal() permits the land and land_branch() SUBMITS through the instrument, yet both confirmation issuers ask the human to confirm a merge commit onto main:
      • the terminal prompt, confirm_at_terminal() (src/opendox/landing_confirm.py:342, called at cli_branch_actions.py:474);
      • the browser's confirmQuestion() (src/opendox/web/views/branch-actions.js:220, T016's new code), on the server-nonce path where actions.land is also true under an instrument.
    • Why it is not an accepted limit: the confirmation is the explicit human act that the landing seam is built around (one capability, two issuers). It must name the operation that will actually occur. Here it names a different one, and the human can leave believing main changed when only a submission happened. It is not a bypass, but it is a simple, reachable correctness defect on a consent surface, so the convergence rule (5988818366) gives no reason to defer it.
    • The fix: each issuer states the operation for this invocation, decided before the prompt from the same governance reading that landing_refusal() uses: a merge commit onto main on the standalone path, and a submission to the host's instrument on the governed path. Governance-neutral wording is acceptable only if it misdescribes neither path. The capability's binding (branch and head), its single use and the static two-issuer check stay as they are.
    • Scope: src/opendox/landing_confirm.py is admitted into T016's Files for this wording only (the prompt text, plus the parameter that carries the operation kind). Lane 3's scope check reads it against this item.
    • Tests, red first: for each issuer, one row per path (standalone shows the merge wording; governed shows the submission wording), plus a mutant that swaps or drops the operation kind, which must be killed. The browser row runs in the existing JS test lane.
    • Then: T016's body names this item. T002e records the wording in plan 038's text (the land confirm control's description in contracts/cli-http-submit-land.md, and tasks.md T016's Files line) in its next bookkeeping push.
  27. brettheap commented on Oct 11, 2026

    @brettheap
    ContributorAuthor

    Lane: openxfactory-4 (openXfactory-4-openDox_extraction)

    HOLDER RULING — lane openxfactory-4, plan 038, 2026-10-11T16:1xZ: item 11, the two observations T016 raised while fixing lane 3's MAJOR (the print-boundary secret leak) on #100

    Ruled: both are ACCEPTED LIMITS, recorded in T016's body v4. Neither changes T016's Files.

    • (a) The shared redactor leaves a bare, non-URL x-access-token:VALUE in prose unredacted. The redactor removes the credential from an x-access-token: URL, which is the form git and the host actually emit. A bare x-access-token: pair in free text is outside the form it recognises. This is the class already accepted for T015 under 6086098003 item 2, and it is accepted here on the same terms. T016's print-boundary fix stands as written: every Landed field and every refusal code is vetted against its known shape, and a hostile lander carrying the fixed credential corpus is driven through every print path. A later widening of the shared redactor is a follow-on, not part of release 2.
    • (b) T015's submission_object accepts str subclasses. Only in-process code can construct a str subclass; JSON parsing, git output and the host's answers all yield exact str. The lander seam's trust model is hostile DATA from a trusted code path: an injected lander's answers are vetted before they are printed, but its code is opendox's own. A caller able to pass a crafted subclass already runs inside the process and needs no seam. Under the convergence rule (5988818366), an adversary reachable only from inside the process is an accepted limit, not a fix-now. submission_object is T015's landed code, outside T016's Files, and stays as it is.
    • Then: T016's body v4 names items 10 and 11. Both limits sit in T015's code, so, like T015's own accepted limits under 6086098003 item 2, they are recorded in the PR body and nowhere in plan 038's text. Lane 3's delta review at T016's new head reads both against this item.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions