DUNIN7 · LOOMWORKS · RECORD
record.dunin7.com
Status Current
Path phases/phase-63-editor-refinement/loomworks-phase-63-step-0-findings-v0_1.md

Phase 63 Step 0 Inspection Findings — v0.1

Date: 2026-05-23 Verification source: phases/phase-63-editor-refinement/loomworks-phase-63-step-0-inspection-brief-v0_1.html (loomworks-record commit 0e8b272) loomworks-engine HEAD: 2f04e7a loomworks-record HEAD: 0e8b272


V1 — The existing setting-change writer pattern

CONFIRMED.

There is exactly one writer helper in src/loomworks/audit/events.py:


async def write_setting_change_event(
    *,
    actor_person_id: UUID,
    setting_key: str,
    previous_value: Any,
    new_value: Any,
    engagement_id: UUID | None,
    db: AsyncSession,
    now: datetime | None = None,
) -> UUID:

Payload shape convention (lines 79-83) is a flat JSONB dict whose keys are documented in the model docstring (models.py:37) — {"setting_key": str, "previous_value": Any | None, "new_value": Any} — coerced through _json_safe(...) (lines 34-49) which falls back to str(value) for non-JSON-encodable inputs.

Discipline notes from the docstring (lines 8-12, 14-16):

Tx mapping (lines 67-73): tx_id = uuid.uuid4() — today every narrative event has a 1:1 row:tx mapping; future multi-row events can share a tx_id. The function does not flush its own transaction; the caller's db.begin() block atomically commits the substrate write and the audit row.

Example call-site (src/loomworks/orchestration/tune_setting.py:83-99):


try:
    await write_setting_change_event(
        actor_person_id=actor_person_id,
        setting_key=setting_key,
        previous_value=previous_value,
        new_value=new_value,
        engagement_id=engagement_id,
        db=db,
    )
except SQLAlchemyError:
    logger.exception(
        "FORAY audit write failed for setting_change "
        "(person=%s, key=%s); continuing — setting write is "
        "authoritative",
        actor_person_id,
        setting_key,
    )

V2 — The audit.foray_events table shape

CONFIRMED. Adding editor_commit as a new event_type requires no schema migration.

Columns (lines 36-82): | column | type | nullable | default | | ----------------- | --------------------------- | -------- | -------------------- | | id | UUID | NOT NULL | gen_random_uuid() | | tx_id | UUID | NOT NULL | — | | event_type | VARCHAR(64) | NOT NULL | — | | actor_person_id | UUID | NOT NULL | — | | engagement_id | UUID | nullable | — | | payload | JSONB | NOT NULL | '{}'::jsonb | | signature | LargeBinary (BYTEA) | nullable | — | | timestamp | TIMESTAMP WITH TIME ZONE | NOT NULL | now() |

Indexes (lines 84-110): tx_id; (actor_person_id, timestamp DESC); (event_type, timestamp DESC); (engagement_id, timestamp DESC).

Constraints that would block "editor_commit" as a value: none. No CHECK on event_type; no enum type; VARCHAR(64) accepts any string up to 64 chars. "editor_commit" is 13 chars — well within bounds. Migration docstring states explicitly (lines 7-12): "v0.1 ships one event type (setting_change); the table shape is structurally extensible to future 'metadata-not-content' intents (role changes, identity changes, workspace operations per the parent CR §F) by adding new event_type values + payload schemas." Discriminator-column comment (line 50): "Future: 'role_change', 'identity_change', 'workspace_operation', etc."

Model docstring (audit/models.py:36-38) reinforces: "event_type discriminates payload shape. v0.1: \"setting_change\" only … Future event types extend this enum-by-convention."

actor_person_id is NOT NULL — Phase 63 must supply a valid person UUID for editor commits (see V10 below for the resolution gap). engagement_id is nullable, fits editor commits which are not engagement-scoped today.


V3 — The best-effort writing convention

CONFIRMED — and uniform across both call sites.

Call site 1: src/loomworks/orchestration/tune_setting.py (Companion-spoken path) Wrapped through a thin helper _audit_setting_change (lines 65-99). Critical excerpt:


try:
    await write_setting_change_event(
        actor_person_id=actor_person_id,
        ...
        db=db,
    )
except SQLAlchemyError:
    logger.exception(
        "FORAY audit write failed for setting_change "
        "(person=%s, key=%s); continuing — setting write is "
        "authoritative",
        actor_person_id,
        setting_key,
    )

Helper docstring (lines 74-82) names the discipline explicitly: "A failure here MUST NOT abort the setting write — the setting change is already authoritative at this point. Log the exception and swallow it so the handler still returns its tuned/reset operation_data."

Helper called from both numeric (line 596) and enum (line 489) dispatch paths.

Call site 2: src/loomworks/api/routers/me_settings.py (button-clicked PUT path) Lines 116-131:


try:
    await write_setting_change_event(
        actor_person_id=person.id,
        setting_key=setting_key,
        previous_value=previous,
        new_value=normalized,
        engagement_id=None,
        db=db,
    )
except SQLAlchemyError:
    logger.exception(
        "FORAY audit write failed for PUT /me/settings/%s "
        "(person=%s); continuing — setting write is authoritative",
        setting_key,
        person.id,
    )

Both sites: try/except SQLAlchemyError; logger.exception(...) (which logs traceback at ERROR level); continue. No re-raise. Identical pattern. The convention is uniform.

One small structural difference (not load-bearing): tune_setting.py factors the try/except into a private helper, me_settings.py inlines it. Same semantics either way.


V4 — The engine's existing API router conventions

CONFIRMED. FastAPI. Pydantic response models. Cookie + bearer dual-path auth. Opaque-cursor pagination.

Five named existing routers with file paths:

  1. src/loomworks/api/routers/engagements.py — engagement creation & lifecycle (Phase 3 §11).
  2. src/loomworks/api/routers/engagement_list.pyGET /engagements, membership-scoped list.
  3. src/loomworks/api/routers/me_settings.pyGET / PUT /me/settings/{setting_key} (V1/V3's writer call site).
  4. src/loomworks/api/routers/dashboard.py — Phase 39 cross-engagement aggregation, three endpoints, cursor-paginated.
  5. src/loomworks/api/routers/admin_grants.py — admin grant list/audit, cursor-paginated.

Auth dependencies (defined in src/loomworks/api/deps.py):

Response-model shape: Pydantic v2 BaseModel subclasses; route decorators carry response_model=.... From me_settings.py:45-51:


class SettingResponse(BaseModel):
    setting_key: str
    value: Any


class SettingRequest(BaseModel):
    value: Any

One example route signature with auth dependency (me_settings.py:73-81):


@router.put(
    "/me/settings/{setting_key}", response_model=SettingResponse
)
async def put_setting_route(
    setting_key: str,
    body: SettingRequest,
    person: Person = Depends(get_current_person),
    db: AsyncSession = Depends(get_db_session),
) -> SettingResponse:

Pagination pattern: opaque-cursor pagination. Two known implementations:

Some assertion-listing endpoints use limit/offset directly (e.g. assertions.py:386 "Pagination is server-enforced — limit caps at 200 even if a caller …") but cursor is the canonical pattern for newer routes and would be the natural choice for an event_type=editor_commit audit-feed endpoint.


V5 — The engine's API host and exposure

NUANCED. Engine deployment at api.loomworks.doneinseven.com is named in scoping but not yet live; CORS infrastructure exists and is configured to allow an unspecified set of production origins via env var.

This nuance is recorded in Phase 51/52 implementation notes, not a contradiction of the brief — the brief itself names this premise. But it surfaces as material for Phase 63: the editor's Pages Function will be calling an engine that does not yet exist in production.

CORS configuration (src/loomworks/api/app.py:386-419):


cors_origins: list[str] = [
    o.strip()
    for o in (settings.loomworks_cors_origins or "").split(",")
    if o.strip()
]
if settings.loomworks_env in ("development", "test"):
    cors_origins = [
        "http://localhost:3000",
        "http://localhost:3001",
        *cors_origins,
    ]
if cors_origins:
    app.add_middleware(
        CORSMiddleware,
        allow_origins=cors_origins,
        allow_credentials=True,
        allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
        allow_headers=["Content-Type"],
    )

Authentication mechanisms:

What would block a Cloudflare Pages Function at record.dunin7.com calling the engine API directly:

  1. Engine not yet deployed in production. Memory entry "Phase 51 closed" suggests engine lives at "api.loomworks.dunin7.com via M4 tunnel" — but Phase 51/52 impl notes treat this as future work.
  2. CORS origin allowlist must list https://record.dunin7.com in LOOMWORKS_CORS_ORIGINS once the engine is reachable.
  3. allow_headers=["Content-Type"] — the Pages Function cannot pass a custom header (e.g. X-Editor-Token) without engine-side widening of this list, or it must use cookies (no SameSite issue if both are subdomains of dunin7.com).
  4. No matching auth mechanism for service-to-service calls today. Bearer tokens are engagement-scoped contributor tokens; session cookies are WebAuthn+TOTP-issued and not portable to a server-to-server context. A Pages Function calling the engine would need either (a) a new service-token mechanism, (b) some shared secret config, or (c) the engine to honor Cloudflare-Access JWT directly. None of those exist today.
  5. Operator UUID resolution endpoint does not exist (see V10) — even if connectivity worked, the Pages Function has no way to ask the engine "what UUID is this email?".

Load-bearing for Phase 63: The "missing service-to-service auth" gap (#4) and the "no UUID resolution endpoint" gap (#5) together mean Phase 63 Piece 3 must scope at least one of (a) a new engine endpoint + auth mechanism, or (b) some alternate path for the Pages Function to learn the operator UUID. The CORS and engine-deployment items are operational; the auth/endpoint items are CR-level.


V6 — Whether any read-API surface for FORAY tables exists

CONFIRMED. No HTTP read endpoint exists for FORAY tables — neither audit.foray_events nor credit.foray_action_flows.

Search command run: grep -rn "foray_events\|audit\." src/loomworks/api/ src/loomworks/orchestration/routers/. Result: no HTTP route references audit.foray_events; the only mentions are the write paths from V3 and documentation comments. No router file reads from the table.

Search command for credit.foray_action_flows: similar result — the table is referenced from credit/flows.py, credit/reconciliation_evaluator.py, credit/specialists.py, credit/proposal_applier.py, and credit/bootstrap.py, but no HTTP route. The reconciliation evaluator (background task started in app.py:303-307) reads from it internally; nothing exposes it externally.

Internal selects that could be lifted into an API surface: For audit.foray_events, no internal select exists at all today — the table is write-only post-migration. Adding a read endpoint for Piece 3 (the audit viewer) is greenfield. The four indexes on the table (tx_id; actor_person_id+ts DESC; event_type+ts DESC; engagement_id+ts DESC) anticipate exactly the kinds of queries a read endpoint would do:

This confirms the brief's premise: Piece 3 must build the read endpoint from scratch; the table's index choices already pre-imagine the query shapes.


V7 — Current version-bump implementation in the editor

CONFIRMED. No content transformation happens — the editor sends content unchanged; save.ts archives the old content unchanged.

Key sub-steps of the version-bump path:

  1. Parse the filename (line 109): const parsed = parseVersionedFilename(filename); — returns {base, ext, major, minor, patch}.
  2. Compute the new version (line 120): const newParsed = { ...parsed, minor: parsed.minor + 1, patch: 0 }; — bumps minor by 1, resets patch to 0. Comment confirms (lines 118-119): "Phase 62 supports minor bumps only (v0.1 -> v0.2). Patch and major bumps are future-facing affordances."
  3. Compute paths (lines 121-123):

```typescript const newFilename = buildVersionedFilename(newParsed); const newPath = ${folder}${newFilename}; const archivePath = ${folder}archive/${filename}; ```

  1. Collision check (lines 125-141): if newPath exists, return 409.
  2. Fetch current content (lines 143-152): currentContent = await githubGetFile(env, validatedPath); — used to copy unchanged to archive.
  3. Atomic multi-file commit (lines 161-170):

```typescript const commitSha = await githubMultiFileCommit( env, [ { path: newPath, content: body.content }, // new content from request { path: archivePath, content: currentContent.content }, // old content unchanged ], [validatedPath], // delete old path message ); ```

Confirmation that no content transformation happens: body.content is written verbatim to newPath; currentContent.content (just fetched from GitHub) is written verbatim to archivePath. Nothing reads or rewrites version strings; there is no scanner step today.

Natural attachment point for a version-string scanner (Piece 1):

Between line 152 (after currentContent is fetched) and line 161 (before githubMultiFileCommit is invoked). At that point all the inputs are available:

The scanner would:

  1. Walk body.content looking for the patterns enumerated in V8.
  2. Surface proposed substitutions to the operator for approval.
  3. Return a transformed-content (and a side-by-side diff) to the editor for confirmation before the commit lands.

A second-layer attachment is in tools/static/editor.js:153-155 — the "Save as new version" click handler can intercept before posting, scan the content client-side, and present the diff inline; the server-side scanner remains the source of truth, but a client-side preview avoids a round-trip.


V8 — Version-string patterns present in documents today

CONFIRMED. Multiple distinct patterns. The scanner needs to recognize each. Patterns enumerated below by example.

Pattern A — vN.M in <title> element

Example: architecture/loomworks-architecture-specification-v0_3.html:5


<title>Loomworks Architecture Specification — v0.3</title>

Also: operations/going-forward-operating-instructions-v0_3.html:5


<title>Going-forward operating instructions v0.3</title>

Also: scoping-notes/loomworks-phase-63-scoping-note-v0_1.html:5


<title>Loomworks — Phase 63 Scoping Note v0.1 — Editor Refinement with FORAY Audit Substrate</title>

Pattern B — vN.M in document-meta / version-line element (early in document)

Example: architecture/loomworks-architecture-specification-v0_3.html:644


<div class="doc-version-line">v0.3<span class="sep">·</span>2026-05-21<span class="sep">·</span>Working draft</div>

Example: architecture/loomworks-architecture-specification-v0_3.html:616


<div class="sidebar-subtitle">Architecture spec · v0.3</div>

Example: operations/going-forward-operating-instructions-v0_3.html:71


<div class="doc-meta">v0.3<span class="sep">·</span>2026-05-22<span class="sep">·</span>Working draft</div>

Example: scoping-notes/loomworks-phase-63-scoping-note-v0_1.html:270


v0.1<span class="sep">·</span>2026-05-23<span class="sep">·</span>Working draft

Pattern C — vN.M in footer (often stale; this is the live bug)

Example: architecture/loomworks-architecture-specification-v0_3.html:1677


<p>Loomworks Architecture Specification · v0.1 · 2026-05-21</p>

This is exactly the bug Phase 63 Piece 1 is named to fix. The filename, title, and version-line all say v0.3; the footer still says v0.1.

Example: operations/going-forward-operating-instructions-v0_3.html:274


<p>Loomworks · Going-forward operating instructions · 2026-05-22 · v0.3</p>

(This footer reads correctly — the operating-instructions doc was apparently hand-corrected at version-bump time.)

Example: scoping-notes/loomworks-phase-63-scoping-note-v0_1.html:503


Loomworks · Phase 63 Scoping Note · v0.1 · 2026-05-23

Pattern D — "from vN.M to vN.M+1" transition prose

Example: scoping-notes/loomworks-phase-63-scoping-note-v0_1.html:284 > "...the editor was used to update the Loomworks Architecture Specification from v0.2 to v0.3 in preparation for substantive content work."

Example: operations/going-forward-operating-instructions-v0_3.html:77 > "This document supersedes v0.2. Earlier versions are preserved in archive/ for trajectory."

Example: operations/going-forward-operating-instructions-v0_3.html:81 > "What changed from v0.2"

Example: operations/going-forward-operating-instructions-v0_3.html:84 > "...v0.2 named cross-session sync..."

Note: This pattern is the most semantically fraught. References to historical predecessor versions ("we considered X in v0.1 but rejected it" — scoping note line 352) are intentionally backward-looking and MUST NOT be auto-bumped. The scoping-note §A.4 calls this out explicitly: "intentional historical references to old versions ... are preserved because the operator sees each occurrence individually." This is the strongest argument for the propose-and-approve flow rather than auto-substitute.

Pattern E — vN.M → vN.M / vN.M_M+1 / vN_M inside prose

Example: operations/going-forward-operating-instructions-v0_3.html:211 > "all versions are working drafts and increments are minor (v0.1 → v0.2 → v0.3). The transition to v1.0 ..."

Example: operations/going-forward-operating-instructions-v0_3.html:218 > "New documents start at v0_1..."

Example: operations/going-forward-operating-instructions-v0_3.html:219 > "...The version-bump UI on save produces v0_2 from v0_1, etc., and atomically moves the previous version to archive/"

The underscore form (v0_1 / v0_2) appears in inline <code> tags, matching the filename convention. Document-meta version-lines use the dotted form (v0.1); prose mixes both.

Pattern F — Cross-document references with embedded versions

Example: operations/going-forward-operating-instructions-v0_3.html:248 > "lives in the methodology document (what-dunin7-is-building-v0_20.md currently, v0_21 candidate material in current-status-manifest)."

Example: operations/going-forward-operating-instructions-v0_3.html:249 > "(loomworks-architecture-specification-v0_2.html currently)." This is a stale cross-reference — the live architecture spec is at v0.3, but the operating-instructions doc still names v0.2. A scanner that fires on cross-document references would need a way to know which references belong to the doc being bumped vs. unrelated docs.

Example: scoping-notes/loomworks-phase-63-scoping-note-v0_1.html:431 > "They will be carried to the v0.21 methodology consolidation alongside the existing backlog."

Example: scoping-notes/loomworks-phase-63-scoping-note-v0_1.html:490 > "...the first absorption since v0.41 — closing several other phases..."

Summary of patterns the scanner needs to recognize

| Pattern | Form | Examples per file | | ------- | ---- | ----------------- | | A | vN.M in <title> | 1 per doc | | B | vN.M in doc-meta / sidebar / version-line near top | 1-2 per doc | | C | vN.M in footer paragraph | 1 per doc | | D | "from vN.M to vN.M+1" / "supersedes vN.M" / "in vN.M we ..." prose | several | | E | v0.1 → v0.2 arrow notation and v0_1 / v0_2 underscore form in <code> | several | | F | Cross-document filename references e.g. name-v0_2.html | mixed; excluded from bump |

Critical design constraint surfaced: Patterns D, E, F are intentionally polysemic. Pattern D backward-references ("we considered X in v0.1") must NOT bump. Pattern F cross-references to OTHER documents must NOT bump (the operator might be inside doc A bumping it from v0.2 to v0.3, but mentions of doc B's v0.2 are unrelated). Pattern A, B, C are almost always safe to auto-substitute when the version under change matches the doc's own version. The scanner's UX must default to propose-not-mutate to handle the polysemic cases — the scoping-note draft already says this.


V9 — The editor's current after-save behavior

CONFIRMED. After a successful version-bump save, the editor remains pointed at the now-archived predecessor: there is no breadcrumb update, no buffer reset, and no redirect to the new path.

The relevant response-handling code (lines 168-197):


const json = await res.json();
if (!res.ok) throw new Error(json.error || res.statusText);

if (payload.mode === "version-bump") {
  setStatus(
    `Saved as new version. Commit ${json.commitSha.slice(0, 8)}. ` +
      `New path: ${json.newPath}. Previous archived.`,
    "success"
  );
} else {
  setStatus(
    `Saved. Commit ${json.commitSha.slice(0, 8)}. ` +
      `The site will rebuild within a minute.`,
    "success"
  );
}

// Re-enable cancel only; the user closes the modal explicitly so they
// see the success message.
document.querySelectorAll(".editor-modal-actions button").forEach((b) => (b.disabled = true));
const cancelBtn = document.querySelector('[data-action="cancel"]');
if (cancelBtn) {
  cancelBtn.disabled = false;
  cancelBtn.textContent = "Close";
}

The status message names json.newPath (the new versioned path) and json.previousPath is also in the response (from save.ts:177), but neither is acted on:

The result: the Save button changes to a "Close" button, the success status is shown, the operator dismisses the modal — and the page they return to (the chrome) still shows them the v0.2 doc, while v0.3 has just been created on disk. The breadcrumb is generated by the static site build (tools/build_site.py), which won't re-render until the Cloudflare Pages build picks up the new commit minutes later.

Natural place an auto-reopen-at-new-path behavior would attach (Piece 2):

Inside saveDocument(...), after line 184 (the success setStatus call) and before line 187 (the button-disable block). At that point:

Action: close the existing modal and call openEditFlow(json.newPath) to reopen against the new versioned path. The reopened modal would do a fresh GET /api/document?path=... so it picks up the just-written content and sha. The operator's mental model — "I'm editing the document; the document is now at v0.3; I can keep editing it" — is preserved.

A secondary attachment point: the page's chrome can be told to update its breadcrumb via custom event or via history.replaceState, so even if the operator closes the modal, the URL-bar-visible path reflects the new version. This is a small extra: the auto-reopen above is the load-bearing fix.


V10 — How the editor knows the operator's UUID today

CONTRADICTED in part — affects Phase 63 Piece 3 substantially. The editor's authoring-identity surface is narrower than the brief's prose suggests.

What the brief's expected finding holds: yes, the Pages Function authenticates via Cloudflare Access (JWT presence-check), and Cf-Access-Authenticated-User-Email is currently the only identity hint the Function can read. Per Phase 62 Finding 3, that header is currently being read with a fallback default of editor@record.dunin7.com, which is what commits attribute to today.

What does NOT hold as written: the engine substrate does have get_person_by_email (src/loomworks/persons/registry.py:108-129), but there is no HTTP endpoint exposing it. Search confirmed: no router imports it; no path resolves email-to-UUID over HTTP. So the email-to-UUID resolution mechanism does not exist yet at the API surface — it would need to be built.

What identity info does the Pages Function actually have access to today:

From functions/api/_shared.ts:124-129:


export function authenticatedEmail(request: Request): string {
  return (
    request.headers.get("Cf-Access-Authenticated-User-Email") ||
    "editor@record.dunin7.com"
  );
}

Phase 62 Finding 3 (per the brief) says the email header is "currently not forwarded successfully." Reading the code: the fallback default IS being used today, so commits attribute to editor@record.dunin7.com. Per Phase 62 close notes (queued item), this is the symptom the brief names. Whether the underlying cause is Access-config or Pages-config is not investigated here — the symptom is what affects Phase 63.

Does any UUID resolution mechanism exist:

Cross-reference engine-side person table:

Endpoint cost for Phase 63 Piece 3: A new endpoint (e.g. POST /persons/resolve-email) would need to be built. Auth model is the open question — see V5 #4. The endpoint would be called from the Pages Function on every editor commit (or cached client-side per session). Pages Functions are stateless and would need to do this lookup each request unless an in-memory map is kept in the editor's static editor.js (which is reachable from the operator's browser, so caching there is reasonable).

Alternative path worth naming: The Cloudflare Access JWT carries a sub claim (the per-Access-user UUID) which is stable and unique per Access user. If Phase 63 verifies the JWT (the work the comment at _shared.ts:102-112 says was deferred), sub is available without an engine round-trip. The Access UUID isn't the engine's person UUID, so Phase 63 would still need a one-time mapping table from Access-sub to person-id — but that mapping changes only on signup, not on every request.

Summary on V10: The brief's text is roughly right but the gap is wider than its prose. There is no resolution endpoint today; both the endpoint AND a service-to-service auth path need to be designed for Piece 3.


V11 — Confirmation that setting changes are the only narrative-event surface

CONFIRMED. Two call sites; no other narrative-event writers exist.

Search: grep -rn "write_setting_change_event\|AuditForayEventRow" /Users/dunin7/loomworks-engine/src/. Result: ten matches across five files.

No third file imports write_setting_change_event or constructs AuditForayEventRow directly. The orchestration _audit_setting_change helper (lines 65-99 of tune_setting.py) is the only wrapper, and it's only called from inside tune_setting.py (lines 489 + 596 — the enum and numeric dispatch paths).

The brief's expectation holds exactly: setting changes are the only narrative-event surface today. Phase 63 Piece 3 adds editor_commit as the second event-type value, and the writer pattern in V1 generalizes one-line straightforward to it.


Summary

Inspection character: mostly confirmed. Eight of eleven verifications return CONFIRMED with no caveats material to CR drafting (V1, V2, V3, V4, V6, V7, V9, V11). The audit substrate is structurally exactly as the brief expected; the writer pattern is uniform across both call sites; the table accepts new event types with no migration; no FORAY read endpoints exist; the version-bump path is content-preserving; the editor stays pointed at the predecessor; only one narrative-event surface exists today.

Two findings warrant attention before CR drafting:

  1. V5 (NUANCED) + V10 (CONTRADICTED in part) together identify a real Piece 3 scoping gap — there is no existing engine-side HTTP mechanism for the Pages Function to either (a) authenticate as a service or (b) resolve email→UUID, AND the production engine itself may not yet be live at api.loomworks.dunin7.com / api.loomworks.doneinseven.com. Phase 63 Piece 3 (audit-viewer + Pages-Function-to-engine path) cannot be scoped without naming:
  1. V8 surfaces operational complexity for Piece 1 worth lifting into the CR: patterns D, E, F are intentionally polysemic — backward references and cross-document references must NOT be auto-bumped. The scoping note already says "propose-not-mutate"; the CR should make this explicit as an acceptance gate (e.g. "scanner surfaces N matches; operator approves each; intentional historical references can be skipped without aborting the bump").

Whether scoping note v0.2 is required before CR drafting opens: Yes — primarily to resolve the V5/V10 Piece 3 gap. The scoping note should name (or explicitly defer with rationale) the Pages-Function-to-engine auth model and the identity-resolution primitive. Without that decision, Piece 3's CR §8 (Test Plan) and §10 (Acceptance gates) cannot be specified. Piece 1 (V8) is a smaller polish — the propose-not-mutate principle can be captured in CR §A.4 (acceptance gates) without a scoping-note revision, but lifting it into v0.2 alongside V5/V10 would close the loop cleanly.

Halt-and-surface events during inspection: None. All eleven verifications completed within the brief's pacing target (~90 minutes effective). All named files existed at expected paths; no codebase drift was discovered. Inspection scope matched expectations.