Skip to content

feat(MUN-0046a): scope agent-key write access to POST /tasks/:taskId/comments - #75

Merged
Arcanada merged 1 commit into
mainfrom
mun-0046a-comments-scope
Sep 7, 2026
Merged

Arcanada merged 1 commit into
mainfrom
mun-0046a-comments-scope

Conversation

@Arcanada

@Arcanada Arcanada commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

MUN-0046a — scope POST /tasks/:taskId/comments for the agent key (second instance of the MUN-0045 omission)

Measured live (receipts/muneral/mac-e30-tasks-verified-20260907T013920Z.json, program repo):
POST /tasks/{id}/comments refused a valid mun_sk_ agent key with 403 "This route is not available to an agent API key. Authenticate as a user, or ask for the route to be scoped (MUN-0043)." — the same unmarked-route default-deny AgentTaskScopeGuard gives every route that
was never marked @AgentScope(...), one route over from POST /tasks (MUN-0045).

The fix — reuse, not widen

Adds @AgentScope('task') to addComment. No new AgentScopeKind, no decorator/guard change: the
route already names its task via :taskId, exactly like GET /tasks/:taskId and
PATCH /tasks/:taskId/status, which already use 'task'. A comment is an act on one specific task
the agent must already be assigned to — the narrowest existing scope fits without inventing one.
Authorship is unaffected: AddCommentDto carries only body; the stored actor comes from
req.actor (ActorInterceptor, resolved from the credential), never from the request.

Binding acceptance, both run

  • Negative control: an agent key without the scope still 403s — proved for both an unassigned
    agent in the same workspace and an agent in a different workspace, 0 comments written either
    way (tasks-agent-scope.e2e.spec.ts, "the negative control: a key without the scope still 403s on
    comments"). Unlike MUN-0045's 'project-write', 'task''s existing semantics answer 403
    uniformly for both cases — no 403-vs-404 deviation to argue for here.
  • Abuse case: an agent key cannot post a comment attributed to a principal it is not — forged
    actorId/actorType/actor in the body are stripped by the global ValidationPipe({whitelist: true}) and ignored; the stored ActivityLog row's actor is the real credential's agent
    (tasks-agent-scope.e2e.spec.ts, "the abuse case...").

The sweep this card also produced

Two fixes for the identical omission (MUN-0045, this PR) means the honest question is "how many,"
not "which route." Full route-by-route table, read from source, not docs:
catalog/mun0046/route-sweep-20260907.md (program repo), receipted at
receipts/muneral/mun0046a-route-sweep-20260907T020600Z.json. Two findings surfaced, neither fixed
here (out of this card's scope, which is the comments route specifically):

  1. Real finding, verified, not not_measured: POST /agents/tasks/:taskId/assign has no
    workspace or ownership check at all — a valid agent key can assign an arbitrary agent to an
    arbitrary task, cross-workspace. Different shape from this card's fix: that route never adopted
    AgentTaskScopeGuard in the first place, so there was no default-deny to fall through.
  2. not_measured, named honestly rather than asserted either way: SolutionLogHeadController's
    two routes never adopted AgentTaskScopeGuard either, so they never got the workspace boundary
    MUN-0043 gave the sibling field-changes routes; whether an equivalent boundary exists at the
    service layer (loadAuthorityContext) was not checked closely enough in this card to say.

Both recorded for a future card — fixing either is a separate scope decision this card did not
measure the need for, and MUN-0045's own precedent ("do not widen beyond the measured route") argues
against folding them in here.

Tests / build

pnpm build clean. pnpm test (repo root): 1287/1314 passed, 24 skipped, 3 failed — all three in
test/mutation-evidence.spec.ts, all tree-hash/git-supplement mismatches against the pre-edit frozen
evidence file (expected: same mechanism MUN-0044/MUN-0045 documented; the assembly mutation harness
covers a wholly unrelated subsystem and was regenerated against this branch's tree before merge, see
the ChangeAdmissionReceipt/v1 below). pnpm lint clean.

ChangeAdmissionReceipt/v1 (compact for the PR-body size limit; the pretty, fully-commented copy is committed at program repo receipts/muneral/change-admission-mun0046a-20260907T022500Z.json)

{"admission":{"verdict":"admitted_with_exemptions","rule":"admitted requires every verdict = verified; admitted_with_exemptions requires every non-verified entity to have an unexpired exemption (owner + expiry); failed without exemption -> refused; not_measured without exemption -> paused_safe or refused.","counts":{"verified":10,"not_measured":32,"failed":0,"exemptions":32}},"captured_at_utc":"2026-09-07T02:25:13Z","change_set":{"base":"cd64f98f2bb1238d55d5449be3aa74e1e65b93d5","files":[{"kind":"doc","node_id":"document:apps/api/docs/agent-read-loop.md","node_ids":["document:apps/api/docs/agent-read-loop.md"],"path":"apps/api/docs/agent-read-loop.md","status":"M"},{"kind":"route","node_id":"code_unit:apps/api/src/tasks/tasks.controller.ts","node_ids":["code_unit:apps/api/src/tasks/tasks.controller.ts","route:DELETE /tasks/:taskId","route:DELETE /tasks/:taskId/checklist/:itemId","route:DELETE /tasks/:taskId/dependencies/:depId","route:GET /tasks/:taskId","route:GET /tasks/:taskId/activity","route:GET /tasks/:taskId/checklist","route:GET /tasks/:taskId/dependencies","route:GET /tasks/project/:projectId","route:PATCH /tasks/:taskId/checklist/:itemId","route:PATCH /tasks/:taskId/status","route:POST /tasks","route:POST /tasks/:taskId/checklist","route:POST /tasks/:taskId/comments","route:POST /tasks/:taskId/dependencies"],"path":"apps/api/src/tasks/tasks.controller.ts","status":"M"},{"kind":"test","path":"apps/api/test/assembly/mutation-results.json","status":"M","uncovered":true},{"kind":"test","node_id":"code_unit:apps/api/test/tasks-agent-scope.e2e.spec.ts","node_ids":["code_unit:apps/api/test/tasks-agent-scope.e2e.spec.ts"],"path":"apps/api/test/tasks-agent-scope.e2e.spec.ts","status":"M"}],"head":"5d22b094d1b4f3f8147aaaa2ba1ba51795282666","mode":"diff"},"decision_ref":"DEC-AUP-0008","exemptions":[{"entity":"route:DELETE /tasks/:taskId","reason":"provides_route canary unconditional (AUP-GRAPH-008); swept in by file-level diff granularity, declaration byte-identical. See notes[shared-route-exemption].","owner":"aup-mun0046 executor (card MUN-0046a, model claude-sonnet-5 under DEC-AUP-0015)","expires_at_utc":"2026-09-08T00:00:00Z","ref":"notes[shared-route-exemption]"},{"entity":"route:DELETE /tasks/:taskId/checklist/:itemId","reason":"provides_route canary unconditional (AUP-GRAPH-008); swept in by file-level diff granularity, declaration byte-identical. See notes[shared-route-exemption].","owner":"aup-mun0046 executor (card MUN-0046a, model claude-sonnet-5 under DEC-AUP-0015)","expires_at_utc":"2026-09-08T00:00:00Z","ref":"notes[shared-route-exemption]"},{"entity":"route:DELETE /tasks/:taskId/dependencies/:depId","reason":"provides_route canary unconditional (AUP-GRAPH-008); swept in by file-level diff granularity, declaration byte-identical. See notes[shared-route-exemption].","owner":"aup-mun0046 executor (card MUN-0046a, model claude-sonnet-5 under DEC-AUP-0015)","expires_at_utc":"2026-09-08T00:00:00Z","ref":"notes[shared-route-exemption]"},{"entity":"route:GET /tasks/:taskId","reason":"provides_route canary unconditional (AUP-GRAPH-008); swept in by file-level diff granularity, declaration byte-identical. See notes[shared-route-exemption].","owner":"aup-mun0046 executor (card MUN-0046a, model claude-sonnet-5 under DEC-AUP-0015)","expires_at_utc":"2026-09-08T00:00:00Z","ref":"notes[shared-route-exemption]"},{"entity":"route:GET /tasks/:taskId/activity","reason":"provides_route canary unconditional (AUP-GRAPH-008); swept in by file-level diff granularity, declaration byte-identical. See notes[shared-route-exemption].","owner":"aup-mun0046 executor (card MUN-0046a, model claude-sonnet-5 under DEC-AUP-0015)","expires_at_utc":"2026-09-08T00:00:00Z","ref":"notes[shared-route-exemption]"},{"entity":"route:GET /tasks/:taskId/checklist","reason":"provides_route canary unconditional (AUP-GRAPH-008); swept in by file-level diff granularity, declaration byte-identical. See notes[shared-route-exemption].","owner":"aup-mun0046 executor (card MUN-0046a, model claude-sonnet-5 under DEC-AUP-0015)","expires_at_utc":"2026-09-08T00:00:00Z","ref":"notes[shared-route-exemption]"},{"entity":"route:GET /tasks/:taskId/dependencies","reason":"provides_route canary unconditional (AUP-GRAPH-008); swept in by file-level diff granularity, declaration byte-identical. See notes[shared-route-exemption].","owner":"aup-mun0046 executor (card MUN-0046a, model claude-sonnet-5 under DEC-AUP-0015)","expires_at_utc":"2026-09-08T00:00:00Z","ref":"notes[shared-route-exemption]"},{"entity":"route:GET /tasks/project/:projectId","reason":"provides_route canary unconditional (AUP-GRAPH-008); swept in by file-level diff granularity, declaration byte-identical. See notes[shared-route-exemption].","owner":"aup-mun0046 executor (card MUN-0046a, model claude-sonnet-5 under DEC-AUP-0015)","expires_at_utc":"2026-09-08T00:00:00Z","ref":"notes[shared-route-exemption]"},{"entity":"route:PATCH /tasks/:taskId/checklist/:itemId","reason":"provides_route canary unconditional (AUP-GRAPH-008); swept in by file-level diff granularity, declaration byte-identical. See notes[shared-route-exemption].","owner":"aup-mun0046 executor (card MUN-0046a, model claude-sonnet-5 under DEC-AUP-0015)","expires_at_utc":"2026-09-08T00:00:00Z","ref":"notes[shared-route-exemption]"},{"entity":"route:PATCH /tasks/:taskId/status","reason":"provides_route canary unconditional (AUP-GRAPH-008); swept in by file-level diff granularity, declaration byte-identical. See notes[shared-route-exemption].","owner":"aup-mun0046 executor (card MUN-0046a, model claude-sonnet-5 under DEC-AUP-0015)","expires_at_utc":"2026-09-08T00:00:00Z","ref":"notes[shared-route-exemption]"},{"entity":"route:POST /tasks","reason":"provides_route canary unconditional (AUP-GRAPH-008); swept in by file-level diff granularity, declaration byte-identical. See notes[shared-route-exemption].","owner":"aup-mun0046 executor (card MUN-0046a, model claude-sonnet-5 under DEC-AUP-0015)","expires_at_utc":"2026-09-08T00:00:00Z","ref":"notes[shared-route-exemption]"},{"entity":"route:POST /tasks/:taskId/checklist","reason":"provides_route canary unconditional (AUP-GRAPH-008); swept in by file-level diff granularity, declaration byte-identical. See notes[shared-route-exemption].","owner":"aup-mun0046 executor (card MUN-0046a, model claude-sonnet-5 under DEC-AUP-0015)","expires_at_utc":"2026-09-08T00:00:00Z","ref":"notes[shared-route-exemption]"},{"entity":"route:POST /tasks/:taskId/comments","reason":"provides_route canary unconditional (AUP-GRAPH-008); the entity this PR actually changes, directly tested pre-deploy. See notes[shared-route-exemption].","owner":"aup-mun0046 executor (card MUN-0046a, model claude-sonnet-5 under DEC-AUP-0015)","expires_at_utc":"2026-09-08T00:00:00Z","ref":"notes[shared-route-exemption]"},{"entity":"route:POST /tasks/:taskId/dependencies","reason":"provides_route canary unconditional (AUP-GRAPH-008); swept in by file-level diff granularity, declaration byte-identical. See notes[shared-route-exemption].","owner":"aup-mun0046 executor (card MUN-0046a, model claude-sonnet-5 under DEC-AUP-0015)","expires_at_utc":"2026-09-08T00:00:00Z","ref":"notes[shared-route-exemption]"},{"entity":"code_unit:apps/api/src/tasks/tasks.controller.ts","reason":"declares provides_route edges -> canary unconditional (AUP-GRAPH-008), even though hand-verified for every other requirement. See notes[shared-route-exemption].","owner":"aup-mun0046 executor (card MUN-0046a, model claude-sonnet-5 under DEC-AUP-0015)","expires_at_utc":"2026-09-08T00:00:00Z","ref":"notes[shared-route-exemption]"},{"entity":"code_unit:apps/web/__tests__/kanban-board.test.ts","reason":"inferred cross-service hop (apps/web via packages/types, unchanged) -> P6 canary mandatory regardless of static evidence. See notes[shared-web-exemption].","owner":"aup-mun0046 executor (card MUN-0046a, model claude-sonnet-5 under DEC-AUP-0015)","expires_at_utc":"2026-09-08T00:00:00Z","ref":"notes[shared-web-exemption]"},{"entity":"code_unit:apps/web/app/(app)/workspaces/[wsSlug]/dashboard/page.tsx","reason":"inferred cross-service hop (apps/web via packages/types, unchanged) -> P6 canary mandatory regardless of static evidence. See notes[shared-web-exemption].","owner":"aup-mun0046 executor (card MUN-0046a, model claude-sonnet-5 under DEC-AUP-0015)","expires_at_utc":"2026-09-08T00:00:00Z","ref":"notes[shared-web-exemption]"},{"entity":"code_unit:apps/web/app/(app)/workspaces/[wsSlug]/projects/[projSlug]/page.tsx","reason":"inferred cross-service hop (apps/web via packages/types, unchanged) -> P6 canary mandatory regardless of static evidence. See notes[shared-web-exemption].","owner":"aup-mun0046 executor (card MUN-0046a, model claude-sonnet-5 under DEC-AUP-0015)","expires_at_utc":"2026-09-08T00:00:00Z","ref":"notes[shared-web-exemption]"},{"entity":"code_unit:apps/web/app/(app)/workspaces/[wsSlug]/projects/[projSlug]/tasks/[taskId]/page.tsx","reason":"inferred cross-service hop (apps/web via packages/types, unchanged) -> P6 canary mandatory regardless of static evidence. See notes[shared-web-exemption].","owner":"aup-mun0046 executor (card MUN-0046a, model claude-sonnet-5 under DEC-AUP-0015)","expires_at_utc":"2026-09-08T00:00:00Z","ref":"notes[shared-web-exemption]"},{"entity":"code_unit:apps/web/components/dashboard/BlockersList.tsx","reason":"inferred cross-service hop (apps/web via packages/types, unchanged) -> P6 canary mandatory regardless of static evidence. See notes[shared-web-exemption].","owner":"aup-mun0046 executor (card MUN-0046a, model claude-sonnet-5 under DEC-AUP-0015)","expires_at_utc":"2026-09-08T00:00:00Z","ref":"notes[shared-web-exemption]"},{"entity":"code_unit:apps/web/components/kanban/KanbanBoard.tsx","reason":"inferred cross-service hop (apps/web via packages/types, unchanged) -> P6 canary mandatory regardless of static evidence. See notes[shared-web-exemption].","owner":"aup-mun0046 executor (card MUN-0046a, model claude-sonnet-5 under DEC-AUP-0015)","expires_at_utc":"2026-09-08T00:00:00Z","ref":"notes[shared-web-exemption]"},{"entity":"code_unit:apps/web/components/kanban/KanbanCard.tsx","reason":"inferred cross-service hop (apps/web via packages/types, unchanged) -> P6 canary mandatory regardless of static evidence. See notes[shared-web-exemption].","owner":"aup-mun0046 executor (card MUN-0046a, model claude-sonnet-5 under DEC-AUP-0015)","expires_at_utc":"2026-09-08T00:00:00Z","ref":"notes[shared-web-exemption]"},{"entity":"code_unit:apps/web/components/kanban/KanbanColumn.tsx","reason":"inferred cross-service hop (apps/web via packages/types, unchanged) -> P6 canary mandatory regardless of static evidence. See notes[shared-web-exemption].","owner":"aup-mun0046 executor (card MUN-0046a, model claude-sonnet-5 under DEC-AUP-0015)","expires_at_utc":"2026-09-08T00:00:00Z","ref":"notes[shared-web-exemption]"},{"entity":"code_unit:apps/web/components/task/AuditLog.tsx","reason":"inferred cross-service hop (apps/web via packages/types, unchanged) -> P6 canary mandatory regardless of static evidence. See notes[shared-web-exemption].","owner":"aup-mun0046 executor (card MUN-0046a, model claude-sonnet-5 under DEC-AUP-0015)","expires_at_utc":"2026-09-08T00:00:00Z","ref":"notes[shared-web-exemption]"},{"entity":"code_unit:apps/web/components/task/TaskChecklist.tsx","reason":"inferred cross-service hop (apps/web via packages/types, unchanged) -> P6 canary mandatory regardless of static evidence. See notes[shared-web-exemption].","owner":"aup-mun0046 executor (card MUN-0046a, model claude-sonnet-5 under DEC-AUP-0015)","expires_at_utc":"2026-09-08T00:00:00Z","ref":"notes[shared-web-exemption]"},{"entity":"code_unit:apps/web/components/task/TaskDependencies.tsx","reason":"inferred cross-service hop (apps/web via packages/types, unchanged) -> P6 canary mandatory regardless of static evidence. See notes[shared-web-exemption].","owner":"aup-mun0046 executor (card MUN-0046a, model claude-sonnet-5 under DEC-AUP-0015)","expires_at_utc":"2026-09-08T00:00:00Z","ref":"notes[shared-web-exemption]"},{"entity":"code_unit:apps/web/components/task/TaskDetail.tsx","reason":"inferred cross-service hop (apps/web via packages/types, unchanged) -> P6 canary mandatory regardless of static evidence. See notes[shared-web-exemption].","owner":"aup-mun0046 executor (card MUN-0046a, model claude-sonnet-5 under DEC-AUP-0015)","expires_at_utc":"2026-09-08T00:00:00Z","ref":"notes[shared-web-exemption]"},{"entity":"code_unit:apps/web/lib/api/tasks.ts","reason":"inferred cross-service hop (apps/web via packages/types, unchanged) -> P6 canary mandatory regardless of static evidence. See notes[shared-web-exemption].","owner":"aup-mun0046 executor (card MUN-0046a, model claude-sonnet-5 under DEC-AUP-0015)","expires_at_utc":"2026-09-08T00:00:00Z","ref":"notes[shared-web-exemption]"},{"entity":"deployable_unit:apps/web","reason":"inferred cross-service hop (apps/web via packages/types, unchanged) -> P6 canary mandatory regardless of static evidence. See notes[shared-web-exemption].","owner":"aup-mun0046 executor (card MUN-0046a, model claude-sonnet-5 under DEC-AUP-0015)","expires_at_utc":"2026-09-08T00:00:00Z","ref":"notes[shared-web-exemption]"},{"entity":"work_item:MUN-0040","reason":"no local verifier for a work_item by design (GRAPH-006). See notes[work-item-exemption].","owner":"aup-mun0046 executor (card MUN-0046a, model claude-sonnet-5 under DEC-AUP-0015)","expires_at_utc":"2026-09-08T00:00:00Z","ref":"notes[work-item-exemption]"},{"entity":"work_item:MUN-0043","reason":"no local verifier for a work_item by design (GRAPH-006). See notes[work-item-exemption].","owner":"aup-mun0046 executor (card MUN-0046a, model claude-sonnet-5 under DEC-AUP-0015)","expires_at_utc":"2026-09-08T00:00:00Z","ref":"notes[work-item-exemption]"},{"entity":"work_item:MUN-0045","reason":"no local verifier for a work_item by design (GRAPH-006). See notes[work-item-exemption].","owner":"aup-mun0046 executor (card MUN-0046a, model claude-sonnet-5 under DEC-AUP-0015)","expires_at_utc":"2026-09-08T00:00:00Z","ref":"notes[work-item-exemption]"}],"graph":{"builder_version":"1.0.0","built_at_utc":"2026-09-07T02:24:15Z","graph_digest":"sha256:38c2674c186f6defd30d533d9fb72d2b2ef37562343b7f1342a6cd1320be6b4d","path":"/run/user/1002/orca-claude/claude-1002/-home-dev/a31610d6-d491-4616-9c73-f94008cd6a0f/scratchpad/mun0046-graph/base-graph.json","source_commit":"cd64f98f2bb1238d55d5449be3aa74e1e65b93d5"},"host":"arcana-devs","impact_set":{"deterministic_core":[{"boundary":"intra_unit","depth":2,"entity":"code_unit:apps/api/src/app.module.ts","node_type":"code_unit","path":[{"edge_type":"imports","from":"code_unit:apps/api/src/tasks/tasks.module.ts","provenance":"deterministic","to":"code_unit:apps/api/src/tasks/tasks.controller.ts"},{"edge_type":"imports","from":"code_unit:apps/api/src/app.module.ts","provenance":"deterministic","to":"code_unit:apps/api/src/tasks/tasks.module.ts"}]},{"boundary":"intra_unit","depth":3,"entity":"code_unit:apps/api/src/main.ts","node_type":"code_unit","path":[{"edge_type":"imports","from":"code_unit:apps/api/src/tasks/tasks.module.ts","provenance":"deterministic","to":"code_unit:apps/api/src/tasks/tasks.controller.ts"},{"edge_type":"imports","from":"code_unit:apps/api/src/app.module.ts","provenance":"deterministic","to":"code_unit:apps/api/src/tasks/tasks.module.ts"},{"edge_type":"imports","from":"code_unit:apps/api/src/main.ts","provenance":"deterministic","to":"code_unit:apps/api/src/app.module.ts"}]},{"boundary":"intra_unit","depth":1,"entity":"code_unit:apps/api/src/tasks/tasks.module.ts","node_type":"code_unit","path":[{"edge_type":"imports","from":"code_unit:apps/api/src/tasks/tasks.module.ts","provenance":"deterministic","to":"code_unit:apps/api/src/tasks/tasks.controller.ts"}]},{"boundary":"intra_unit","depth":2,"entity":"code_unit:apps/api/test/app-module-boot.e2e.spec.ts","node_type":"code_unit","path":[{"edge_type":"imports","from":"code_unit:apps/api/src/tasks/tasks.module.ts","provenance":"deterministic","to":"code_unit:apps/api/src/tasks/tasks.controller.ts"},{"edge_type":"imports","from":"code_unit:apps/api/test/app-module-boot.e2e.spec.ts","provenance":"deterministic","to":"code_unit:apps/api/src/tasks/tasks.module.ts"}]},{"boundary":"intra_unit","depth":2,"entity":"code_unit:apps/api/test/field-changes.e2e.spec.ts","node_type":"code_unit","path":[{"edge_type":"imports","from":"code_unit:apps/api/src/tasks/tasks.module.ts","provenance":"deterministic","to":"code_unit:apps/api/src/tasks/tasks.controller.ts"},{"edge_type":"imports","from":"code_unit:apps/api/test/field-changes.e2e.spec.ts","provenance":"deterministic","to":"code_unit:apps/api/src/tasks/tasks.module.ts"}]},{"boundary":"intra_unit","depth":1,"entity":"deployable_unit:apps/api","node_type":"deployable_unit","path":[{"edge_type":"deploys_to","from":"code_unit:apps/api/src/tasks/tasks.controller.ts","provenance":"deterministic","to":"deployable_unit:apps/api"}]},{"boundary":"intra_unit","depth":1,"entity":"document:README.md","node_type":"document","path":[{"edge_type":"documents","from":"document:README.md","provenance":"deterministic","to":"route:GET /tasks/:taskId"}]},{"boundary":"intra_unit","depth":1,"entity":"document:apps/api/docs/migration-import.md","node_type":"document","path":[{"edge_type":"documents","from":"document:apps/api/docs/migration-import.md","provenance":"deterministic","to":"route:PATCH /tasks/:taskId/status"}]},{"boundary":"intra_unit","depth":3,"entity":"work_item:MUN-0040","node_type":"work_item","path":[{"edge_type":"imports","from":"code_unit:apps/api/src/tasks/tasks.module.ts","provenance":"deterministic","to":"code_unit:apps/api/src/tasks/tasks.controller.ts"},{"edge_type":"imports","from":"code_unit:apps/api/test/app-module-boot.e2e.spec.ts","provenance":"deterministic","to":"code_unit:apps/api/src/tasks/tasks.module.ts"},{"edge_type":"documents","from":"work_item:MUN-0040","provenance":"deterministic","to":"code_unit:apps/api/test/app-module-boot.e2e.spec.ts"}]},{"boundary":"intra_unit","depth":1,"entity":"work_item:MUN-0043","node_type":"work_item","path":[{"edge_type":"documents","from":"work_item:MUN-0043","provenance":"deterministic","to":"code_unit:apps/api/src/tasks/tasks.controller.ts"}]},{"boundary":"intra_unit","depth":1,"entity":"work_item:MUN-0045","node_type":"work_item","path":[{"edge_type":"documents","from":"work_item:MUN-0045","provenance":"deterministic","to":"code_unit:apps/api/src/tasks/tasks.controller.ts"}]}],"edge_types":"all","global_fallback":{"triggered":false},"inferred_tail":[{"boundary":"service","depth":2,"entity":"code_unit:apps/web/__tests__/kanban-board.test.ts","node_type":"code_unit","path":[{"boundary":"service","edge_type":"consumes_contract","from":"code_unit:apps/web/lib/api/tasks.ts","provenance":"inferred","to":"route:GET /tasks/:taskId"},{"edge_type":"imports","from":"code_unit:apps/web/__tests__/kanban-board.test.ts","provenance":"deterministic","to":"code_unit:apps/web/lib/api/tasks.ts"}]},{"boundary":"service","depth":3,"entity":"code_unit:apps/web/app/(app)/workspaces/[wsSlug]/dashboard/page.tsx","node_type":"code_unit","path":[{"boundary":"service","edge_type":"consumes_contract","from":"code_unit:apps/web/lib/api/tasks.ts","provenance":"inferred","to":"route:GET /tasks/:taskId"},{"edge_type":"imports","from":"code_unit:apps/web/components/dashboard/BlockersList.tsx","provenance":"deterministic","to":"code_unit:apps/web/lib/api/tasks.ts"},{"edge_type":"imports","from":"code_unit:apps/web/app/(app)/workspaces/[wsSlug]/dashboard/page.tsx","provenance":"deterministic","to":"code_unit:apps/web/components/dashboard/BlockersList.tsx"}]},{"boundary":"service","depth":2,"entity":"code_unit:apps/web/app/(app)/workspaces/[wsSlug]/projects/[projSlug]/page.tsx","node_type":"code_unit","path":[{"boundary":"service","edge_type":"consumes_contract","from":"code_unit:apps/web/lib/api/tasks.ts","provenance":"inferred","to":"route:GET /tasks/:taskId"},{"edge_type":"imports","from":"code_unit:apps/web/app/(app)/workspaces/[wsSlug]/projects/[projSlug]/page.tsx","provenance":"deterministic","to":"code_unit:apps/web/lib/api/tasks.ts"}]},{"boundary":"service","depth":2,"entity":"code_unit:apps/web/app/(app)/workspaces/[wsSlug]/projects/[projSlug]/tasks/[taskId]/page.tsx","node_type":"code_unit","path":[{"boundary":"service","edge_type":"consumes_contract","from":"code_unit:apps/web/lib/api/tasks.ts","provenance":"inferred","to":"route:GET /tasks/:taskId"},{"edge_type":"imports","from":"code_unit:apps/web/app/(app)/workspaces/[wsSlug]/projects/[projSlug]/tasks/[taskId]/page.tsx","provenance":"deterministic","to":"code_unit:apps/web/lib/api/tasks.ts"}]},{"boundary":"service","depth":2,"entity":"code_unit:apps/web/components/dashboard/BlockersList.tsx","node_type":"code_unit","path":[{"boundary":"service","edge_type":"consumes_contract","from":"code_unit:apps/web/lib/api/tasks.ts","provenance":"inferred","to":"route:GET /tasks/:taskId"},{"edge_type":"imports","from":"code_unit:apps/web/components/dashboard/BlockersList.tsx","provenance":"deterministic","to":"code_unit:apps/web/lib/api/tasks.ts"}]},{"boundary":"service","depth":2,"entity":"code_unit:apps/web/components/kanban/KanbanBoard.tsx","node_type":"code_unit","path":[{"boundary":"service","edge_type":"consumes_contract","from":"code_unit:apps/web/lib/api/tasks.ts","provenance":"inferred","to":"route:GET /tasks/:taskId"},{"edge_type":"imports","from":"code_unit:apps/web/components/kanban/KanbanBoard.tsx","provenance":"deterministic","to":"code_unit:apps/web/lib/api/tasks.ts"}]},{"boundary":"service","depth":2,"entity":"code_unit:apps/web/components/kanban/KanbanCard.tsx","node_type":"code_unit","path":[{"boundary":"service","edge_type":"consumes_contract","from":"code_unit:apps/web/lib/api/tasks.ts","provenance":"inferred","to":"route:GET /tasks/:taskId"},{"edge_type":"imports","from":"code_unit:apps/web/components/kanban/KanbanCard.tsx","provenance":"deterministic","to":"code_unit:apps/web/lib/api/tasks.ts"}]},{"boundary":"service","depth":2,"entity":"code_unit:apps/web/components/kanban/KanbanColumn.tsx","node_type":"code_unit","path":[{"boundary":"service","edge_type":"consumes_contract","from":"code_unit:apps/web/lib/api/tasks.ts","provenance":"inferred","to":"route:GET /tasks/:taskId"},{"edge_type":"imports","from":"code_unit:apps/web/components/kanban/KanbanColumn.tsx","provenance":"deterministic","to":"code_unit:apps/web/lib/api/tasks.ts"}]},{"boundary":"service","depth":2,"entity":"code_unit:apps/web/components/task/AuditLog.tsx","node_type":"code_unit","path":[{"boundary":"service","edge_type":"consumes_contract","from":"code_unit:apps/web/lib/api/tasks.ts","provenance":"inferred","to":"route:GET /tasks/:taskId"},{"edge_type":"imports","from":"code_unit:apps/web/components/task/AuditLog.tsx","provenance":"deterministic","to":"code_unit:apps/web/lib/api/tasks.ts"}]},{"boundary":"service","depth":2,"entity":"code_unit:apps/web/components/task/TaskChecklist.tsx","node_type":"code_unit","path":[{"boundary":"service","edge_type":"consumes_contract","from":"code_unit:apps/web/lib/api/tasks.ts","provenance":"inferred","to":"route:GET /tasks/:taskId"},{"edge_type":"imports","from":"code_unit:apps/web/components/task/TaskChecklist.tsx","provenance":"deterministic","to":"code_unit:apps/web/lib/api/tasks.ts"}]},{"boundary":"service","depth":2,"entity":"code_unit:apps/web/components/task/TaskDependencies.tsx","node_type":"code_unit","path":[{"boundary":"service","edge_type":"consumes_contract","from":"code_unit:apps/web/lib/api/tasks.ts","provenance":"inferred","to":"route:GET /tasks/:taskId"},{"edge_type":"imports","from":"code_unit:apps/web/components/task/TaskDependencies.tsx","provenance":"deterministic","to":"code_unit:apps/web/lib/api/tasks.ts"}]},{"boundary":"service","depth":2,"entity":"code_unit:apps/web/components/task/TaskDetail.tsx","node_type":"code_unit","path":[{"boundary":"service","edge_type":"consumes_contract","from":"code_unit:apps/web/lib/api/tasks.ts","provenance":"inferred","to":"route:GET /tasks/:taskId"},{"edge_type":"imports","from":"code_unit:apps/web/components/task/TaskDetail.tsx","provenance":"deterministic","to":"code_unit:apps/web/lib/api/tasks.ts"}]},{"boundary":"service","depth":1,"entity":"code_unit:apps/web/lib/api/tasks.ts","node_type":"code_unit","path":[{"boundary":"service","edge_type":"consumes_contract","from":"code_unit:apps/web/lib/api/tasks.ts","provenance":"inferred","to":"route:GET /tasks/:taskId"}]},{"boundary":"service","depth":2,"entity":"deployable_unit:apps/web","node_type":"deployable_unit","path":[{"boundary":"service","edge_type":"consumes_contract","from":"code_unit:apps/web/lib/api/tasks.ts","provenance":"inferred","to":"route:GET /tasks/:taskId"},{"edge_type":"deploys_to","from":"code_unit:apps/web/lib/api/tasks.ts","provenance":"deterministic","to":"deployable_unit:apps/web"}]}],"max_depth":3,"method":"reverse traversal (dependents) + forward declared-by hops (provides_route, implements_contract, maps_model of the same file, deploys_to); best path = fewest inferred/observed hops, then shortest; max_depth 3","total":25},"notes":["DRAFT produced by `arcana verify` (tools/graph/verify.py 1.0.0) from matrix contracts/graph-verified-change/verifier-matrix.v1.json; selection rule: required(entity) = ⋃ edge_types[t].mandatory over the edge types t of the hops that reach the entity in its best path (i…","profile: (auto-detected); baseline: contracts/graph-verified-change/fitness-baseline/Arcanada-one__muneral.v1.json (valid); selected: ['targeted_test']; disabled: none","[hand-verify] Three code_unit entities (app.module.ts, main.ts, tasks.controller.ts) came back 'required verifier targeted_test produced no verdict for this entity' from verify.py's own draft, despite real passing test evidence -- a tool-mapping limitation (NestJS e2e specs reach these files by booting a TestingModule / supertest, not by a static import edge the graph's targeted_test selector can follow), not absent evidence. app.module.ts and main.ts are hand-verified from pnpm build (0 errors) plus the fact their only impact-set reason is 'type-check / compile of the importer', which that satisfies. tasks.controller.ts is NOT hand-verified to 'verified' overall -- it also carries the unconditional provides_route canary requirement (AUP-GRAPH-008), which no amount of pre-deploy evidence can close; it remains not_measured, exempted under notes[shared-route-exemption].","[shared-route-exemption] The matrix requires a live canary (AUP-GRAPH-008) for any provides_route edge from a changed declaring file, UNCONDITIONALLY. 15 entities (14 routes + the tasks.controller.ts code_unit) carry this exemption. Of those, only route:POST /tasks/:taskId/comments actually changed behaviour (gained @AgentScope('task')) and it is directly tested (tasks-agent-scope.e2e.spec.ts: positive, negative control, abuse case, all against a real Postgres). The other 13 routes are swept in purely by file-level diff granularity on tasks.controller.ts -- their own declarations are byte-identical before and after. A canary probes the DEPLOYED service; this receipt is written before the merge that triggers deploy, so none can exist yet -- recording any of these 'verified' on test/build strength alone would be the silent totalisation I4 forbids. Evidence gate: a real POST /tasks/:taskId/comments call against the deployed, merged change with the aup-orchestrator agent key, assigned to a real task, succeeding with 201 and correct attribution -- recorded in receipts/muneral/mun0046a-deploy-<ts>.json on the program repo, same shape as mun0045-deploy-20260907T001952Z.json. Reverse_if: the post-deploy probe still answers 403 to a valid in-workspace, task-assigned agent key, or https://api.muneral.com/health does not report a version newer than this PR's base commit (cd64f98) -- either means the change did not actually deploy and the exemption was granted against a deploy that did not happen.","[shared-web-exemption] deployable_unit:apps/web and its 13 inferred-tail code_units are reached only through an inferred, cross-service hop (apps/web importing the tasks API surface via packages/types); P6 makes a live canary mandatory for that boundary regardless of static evidence. packages/types/src/index.ts is UNCHANGED by this PR -- this change adds a server-side authorization scope to one route, no exported type shape moved -- and pnpm build (apps/web, part of the repo-root `pnpm build`) confirms apps/web still compiles against the unchanged contract. What a build cannot supply is proof the DEPLOYED web service still talks to the DEPLOYED api after the deploy this PR triggers. Evidence gate: the apps/web deploy (same CI pipeline, same head) reporting healthy. Reverse_if: the apps/web deploy fails, or a post-deploy smoke check of the task board/detail pages errors on a request this change's server-side surface touches (it should not touch any -- comments are not rendered on those pages today, per grep of apps/web/lib/api/ finding no comments client call).","[work-item-exemption] work_item:MUN-0040/0043/0045 have no local verifier by design (GRAPH-006: the admission receipt is the link to the work item in Muneral, a comment-string reference in files this PR touches, unchanged). Evidence gate: this receipt is embedded in the PR body before merge, which is the link GRAPH-006 requires. Reverse_if: the receipt is not embedded in the PR body, or the referencing comment strings are found to have been altered by this change (they were not -- verified by reading the diff, only @AgentScope('task') and the doc comment changed)."],"producer":{"tool":"tools/graph/build_graph.py + tools/graph/impact.py + tools/graph/contract_diff.py + tools/graph/verify.py (draft) + hand-composed verdicts/exemptions by the admitting agent","version":"1.0.0"},"receipt_id":"change-admission-mun0046a-20260907T022500Z","repo":{"name":"Arcanada-one/muneral","path":"/home/dev/aup/muneral-mun0046 (own clone; --reference /home/dev/aup/muneral --dissociate)"},"schema":"ChangeAdmissionReceipt/v1","staleness":{"checked_at_utc":"2026-09-07T02:25:13Z","method":"manifest.dirty must be false; manifest.source_commit == change_set.base; working tree must be clean (a receipt in diff mode describes committed history only); content_hash of every whole-file node (code_unit, document, receipt; deployable_unit via its package.json) must equal the sha256 of the file bytes at base (git objects); contract/route/data_model hashes are extractor-owned (declaration text) and not recomputed","mismatched_nodes":[],"verdict":"fresh"},"tree":{"commit":"5d22b094d1b4f3f8147aaaa2ba1ba51795282666","dirty":false},"verdicts":[{"entity":"code_unit:apps/api/src/app.module.ts","reason":"HAND-VERIFIED (tool-mapping limitation, real evidence follows): reached only via an 'imports' edge (verifier_hint: type-check / compile of the importer), already satisfied: v-type-check-apps-api-tsconfig (tsc -p tsconfig.json, 0 errors) and v-fitness both passed. The tool's targeted_test mapping has no test file directly importing app.module.ts (NestJS e2e specs boot it via TestingModule, not a direct import edge) -- a tool-mapping limitation, not missing evidence: full `pnpm test` from repo root (1290/1314 passed, 0 failed) exercises the booted module via test/app-module-boot.e2e.spec.ts, which the draft DID verify directly.","verdict":"verified","verifier_ids":["v-fitness","v-type-check-apps-api-tsconfig"]},{"entity":"code_unit:apps/api/src/main.ts","reason":"HAND-VERIFIED (tool-mapping limitation, real evidence follows): same reasoning as app.module.ts: type-check clean, reached only as an importer of the changed decorator/guard chain via app.module.ts, no direct test-file import edge for the tool to map (it is the process entrypoint, never imported by a spec). `pnpm build` compiles it clean; nothing in this change touches its own logic.","verdict":"verified","verifier_ids":["v-fitness","v-type-check-apps-api-tsconfig"]},{"entity":"code_unit:apps/api/src/tasks/tasks.controller.ts","reason":"targeted_test tool-mapping limitation resolved by hand (see notes[hand-verify]); v-canary: INFERRED_BOUNDARY_WITHOUT_CANARY still applies (this file also declares provides_route edges) -- see notes[shared-route-exemption].","verdict":"not_measured","verifier_ids":["v-canary","v-contract-diff","v-fitness","v-type-check-apps-api-tsconfig"]},{"entity":"code_unit:apps/api/src/tasks/tasks.module.ts","reason":"v-type-check-apps-api-tsconfig: apps/api/tsconfig.json: project compiles, 0 errors; v-fitness: no new fitness violation; v-targeted-test-apps-api: test/app-module-boot.e2e.spec.ts: PASS","verdict":"verified","verifier_ids":["v-fitness","v-targeted-test-apps-api","v-type-check-apps-api-tsconfig"]},{"entity":"code_unit:apps/api/test/app-module-boot.e2e.spec.ts","reason":"v-type-check-apps-api-tsconfig-test: apps/api/tsconfig.test.json: project compiles, 0 errors; v-fitness: no new fitness violation; v-targeted-test-apps-api: test/app-module-boot.e2e.spec.ts: PASS","verdict":"verified","verifier_ids":["v-fitness","v-targeted-test-apps-api","v-type-check-apps-api-tsconfig-test"]},{"entity":"code_unit:apps/api/test/field-changes.e2e.spec.ts","reason":"v-type-check-apps-api-tsconfig-test: apps/api/tsconfig.test.json: project compiles, 0 errors; v-fitness: no new fitness violation; v-targeted-test-apps-api: test/field-changes.e2e.spec.ts: PASS","verdict":"verified","verifier_ids":["v-fitness","v-targeted-test-apps-api","v-type-check-apps-api-tsconfig-test"]},{"entity":"code_unit:apps/api/test/tasks-agent-scope.e2e.spec.ts","reason":"v-type-check-apps-api-tsconfig-test: apps/api/tsconfig.test.json: project compiles, 0 errors; v-fitness: no new fitness violation; v-targeted-test-apps-api: test/tasks-agent-scope.e2e.spec.ts: PASS","verdict":"verified","verifier_ids":["v-fitness","v-targeted-test-apps-api","v-type-check-apps-api-tsconfig-test"]},{"entity":"code_unit:apps/web/__tests__/kanban-board.test.ts","reason":"not_measured: inferred cross-service boundary, canary mandatory pre-deploy (P6); exempted.","verdict":"not_measured","verifier_ids":["v-canary","v-contract-diff","v-fitness","v-targeted-test-apps-web","v-type-check-apps-web-tsconfig"]},{"entity":"code_unit:apps/web/app/(app)/workspaces/[wsSlug]/dashboard/page.tsx","reason":"not_measured: inferred cross-service boundary, canary mandatory pre-deploy (P6); exempted.","verdict":"not_measured","verifier_ids":["v-canary","v-contract-diff","v-fitness","v-type-check-apps-web-tsconfig"]},{"entity":"code_unit:apps/web/app/(app)/workspaces/[wsSlug]/projects/[projSlug]/page.tsx","reason":"not_measured: inferred cross-service boundary, canary mandatory pre-deploy (P6); exempted.","verdict":"not_measured","verifier_ids":["v-canary","v-contract-diff","v-fitness","v-type-check-apps-web-tsconfig"]},{"entity":"code_unit:apps/web/app/(app)/workspaces/[wsSlug]/projects/[projSlug]/tasks/[taskId]/page.tsx","reason":"not_measured: inferred cross-service boundary, canary mandatory pre-deploy (P6); exempted.","verdict":"not_measured","verifier_ids":["v-canary","v-contract-diff","v-fitness","v-type-check-apps-web-tsconfig"]},{"entity":"code_unit:apps/web/components/dashboard/BlockersList.tsx","reason":"not_measured: inferred cross-service boundary, canary mandatory pre-deploy (P6); exempted.","verdict":"not_measured","verifier_ids":["v-canary","v-contract-diff","v-fitness","v-type-check-apps-web-tsconfig"]},{"entity":"code_unit:apps/web/components/kanban/KanbanBoard.tsx","reason":"not_measured: inferred cross-service boundary, canary mandatory pre-deploy (P6); exempted.","verdict":"not_measured","verifier_ids":["v-canary","v-contract-diff","v-fitness","v-type-check-apps-web-tsconfig"]},{"entity":"code_unit:apps/web/components/kanban/KanbanCard.tsx","reason":"not_measured: inferred cross-service boundary, canary mandatory pre-deploy (P6); exempted.","verdict":"not_measured","verifier_ids":["v-canary","v-contract-diff","v-fitness","v-type-check-apps-web-tsconfig"]},{"entity":"code_unit:apps/web/components/kanban/KanbanColumn.tsx","reason":"not_measured: inferred cross-service boundary, canary mandatory pre-deploy (P6); exempted.","verdict":"not_measured","verifier_ids":["v-canary","v-contract-diff","v-fitness","v-type-check-apps-web-tsconfig"]},{"entity":"code_unit:apps/web/components/task/AuditLog.tsx","reason":"not_measured: inferred cross-service boundary, canary mandatory pre-deploy (P6); exempted.","verdict":"not_measured","verifier_ids":["v-canary","v-contract-diff","v-fitness","v-type-check-apps-web-tsconfig"]},{"entity":"code_unit:apps/web/components/task/TaskChecklist.tsx","reason":"not_measured: inferred cross-service boundary, canary mandatory pre-deploy (P6); exempted.","verdict":"not_measured","verifier_ids":["v-canary","v-contract-diff","v-fitness","v-type-check-apps-web-tsconfig"]},{"entity":"code_unit:apps/web/components/task/TaskDependencies.tsx","reason":"not_measured: inferred cross-service boundary, canary mandatory pre-deploy (P6); exempted.","verdict":"not_measured","verifier_ids":["v-canary","v-contract-diff","v-fitness","v-type-check-apps-web-tsconfig"]},{"entity":"code_unit:apps/web/components/task/TaskDetail.tsx","reason":"not_measured: inferred cross-service boundary, canary mandatory pre-deploy (P6); exempted.","verdict":"not_measured","verifier_ids":["v-canary","v-contract-diff","v-fitness","v-type-check-apps-web-tsconfig"]},{"entity":"code_unit:apps/web/lib/api/tasks.ts","reason":"not_measured: inferred cross-service boundary, canary mandatory pre-deploy (P6); exempted.","verdict":"not_measured","verifier_ids":["v-canary","v-contract-diff","v-fitness","v-targeted-test-apps-web","v-type-check-apps-web-tsconfig"]},{"entity":"deployable_unit:apps/api","reason":"v-type-check-apps-api-tsconfig: apps/api/tsconfig.json: 0 errors; v-fitness: no new violation in apps/api (5 frozen)","verdict":"verified","verifier_ids":["v-fitness","v-type-check-apps-api-tsconfig"]},{"entity":"deployable_unit:apps/web","reason":"not_measured: inferred cross-service boundary, canary mandatory pre-deploy (P6); exempted.","verdict":"not_measured","verifier_ids":["v-canary","v-fitness","v-type-check-apps-web-tsconfig"]},{"entity":"document:README.md","reason":"v-doc-reference: 2 reference(s) resolve at head","verdict":"verified","verifier_ids":["v-doc-reference"]},{"entity":"document:apps/api/docs/agent-read-loop.md","reason":"v-doc-reference: 7 reference(s) resolve at head","verdict":"verified","verifier_ids":["v-doc-reference"]},{"entity":"document:apps/api/docs/migration-import.md","reason":"v-doc-reference: 10 reference(s) resolve at head","verdict":"verified","verifier_ids":["v-doc-reference"]},{"entity":"route:DELETE /tasks/:taskId","reason":"not_measured: provides_route canary unconditional pre-deploy (AUP-GRAPH-008); exempted.","verdict":"not_measured","verifier_ids":["v-canary","v-contract-diff","v-route-config","v-type-check-apps-api-tsconfig"]},{"entity":"route:DELETE /tasks/:taskId/checklist/:itemId","reason":"not_measured: provides_route canary unconditional pre-deploy (AUP-GRAPH-008); exempted.","verdict":"not_measured","verifier_ids":["v-canary","v-contract-diff","v-route-config","v-type-check-apps-api-tsconfig"]},{"entity":"route:DELETE /tasks/:taskId/dependencies/:depId","reason":"not_measured: provides_route canary unconditional pre-deploy (AUP-GRAPH-008); exempted.","verdict":"not_measured","verifier_ids":["v-canary","v-contract-diff","v-route-config","v-type-check-apps-api-tsconfig"]},{"entity":"route:GET /tasks/:taskId","reason":"not_measured: provides_route canary unconditional pre-deploy (AUP-GRAPH-008); exempted.","verdict":"not_measured","verifier_ids":["v-canary","v-contract-diff","v-route-config","v-type-check-apps-api-tsconfig"]},{"entity":"route:GET /tasks/:taskId/activity","reason":"not_measured: provides_route canary unconditional pre-deploy (AUP-GRAPH-008); exempted.","verdict":"not_measured","verifier_ids":["v-canary","v-contract-diff","v-route-config","v-type-check-apps-api-tsconfig"]},{"entity":"route:GET /tasks/:taskId/checklist","reason":"not_measured: provides_route canary unconditional pre-deploy (AUP-GRAPH-008); exempted.","verdict":"not_measured","verifier_ids":["v-canary","v-contract-diff","v-route-config","v-type-check-apps-api-tsconfig"]},{"entity":"route:GET /tasks/:taskId/dependencies","reason":"not_measured: provides_route canary unconditional pre-deploy (AUP-GRAPH-008); exempted.","verdict":"not_measured","verifier_ids":["v-canary","v-contract-diff","v-route-config","v-type-check-apps-api-tsconfig"]},{"entity":"route:GET /tasks/project/:projectId","reason":"not_measured: provides_route canary unconditional pre-deploy (AUP-GRAPH-008); exempted.","verdict":"not_measured","verifier_ids":["v-canary","v-contract-diff","v-route-config","v-type-check-apps-api-tsconfig"]},{"entity":"route:PATCH /tasks/:taskId/checklist/:itemId","reason":"not_measured: provides_route canary unconditional pre-deploy (AUP-GRAPH-008); exempted.","verdict":"not_measured","verifier_ids":["v-canary","v-contract-diff","v-route-config","v-type-check-apps-api-tsconfig"]},{"entity":"route:PATCH /tasks/:taskId/status","reason":"not_measured: provides_route canary unconditional pre-deploy (AUP-GRAPH-008); exempted.","verdict":"not_measured","verifier_ids":["v-canary","v-contract-diff","v-route-config","v-type-check-apps-api-tsconfig"]},{"entity":"route:POST /tasks","reason":"not_measured: provides_route canary unconditional pre-deploy (AUP-GRAPH-008); exempted.","verdict":"not_measured","verifier_ids":["v-canary","v-contract-diff","v-route-config","v-type-check-apps-api-tsconfig"]},{"entity":"route:POST /tasks/:taskId/checklist","reason":"not_measured: provides_route canary unconditional pre-deploy (AUP-GRAPH-008); exempted.","verdict":"not_measured","verifier_ids":["v-canary","v-contract-diff","v-route-config","v-type-check-apps-api-tsconfig"]},{"entity":"route:POST /tasks/:taskId/comments","reason":"not_measured: provides_route canary unconditional pre-deploy (AUP-GRAPH-008); exempted.","verdict":"not_measured","verifier_ids":["v-canary","v-contract-diff","v-route-config","v-type-check-apps-api-tsconfig"]},{"entity":"route:POST /tasks/:taskId/dependencies","reason":"not_measured: provides_route canary unconditional pre-deploy (AUP-GRAPH-008); exempted.","verdict":"not_measured","verifier_ids":["v-canary","v-contract-diff","v-route-config","v-type-check-apps-api-tsconfig"]},{"entity":"work_item:MUN-0040","reason":"not_measured: no local verifier for a work_item by design (GRAPH-006); exempted.","verdict":"not_measured"},{"entity":"work_item:MUN-0043","reason":"not_measured: no local verifier for a work_item by design (GRAPH-006); exempted.","verdict":"not_measured"},{"entity":"work_item:MUN-0045","reason":"not_measured: no local verifier for a work_item by design (GRAPH-006); exempted.","verdict":"not_measured"}],"verifiers":[{"command":"/home/dev/aup/muneral-mun0046/apps/api/node_modules/.bin/tsc -p /run/user/1002/orca-claude/claude-1002/-home-dev/a31610d6-d491-4616-9c73-f94008cd6a0f/scratchpad/mun0046-graph/verifier-out/tsconfig/apps-api-apps-api-tsconfig-json.json --noEmit --incremental false --listFiles","entities":["code_unit:apps/api/src/app.module.ts","code_unit:apps/api/src/main.ts","code_unit:apps/api/src/tasks/tasks.controller.ts","code_unit:apps/api/src/tasks/tasks.module.ts","deployable_unit:apps/api","route:DELETE /tasks/:taskId","route:DELETE /tasks/:taskId/checklist/:itemId","route:DELETE /tasks/:taskId/dependencies/:depId","route:GET /tasks/:taskId","route:GET /tasks/:taskId/activity","route:GET /tasks/:taskId/checklist","route:GET /tasks/:taskId/dependencies","route:GET /tasks/project/:projectId","route:PATCH /tasks/:taskId/checklist/:itemId","route:PATCH /tasks/:taskId/status","route:POST /tasks","route:POST /tasks/:taskId/checklist","route:POST /tasks/:taskId/comments","route:POST /tasks/:taskId/dependencies"],"exit_code":0,"id":"v-type-check-apps-api-tsconfig","kind":"type_check","output_ref":"/run/user/1002/orca-claude/claude-1002/-home-dev/a31610d6-d491-4616-9c73-f94008cd6a0f/scratchpad/mun0046-graph/verifier-out/v-type-check-apps-api-tsconfig.txt","summary":"apps/api/tsconfig.json: exit 0, 0 error(s), 1166 files listed"},{"command":"/home/dev/aup/muneral-mun0046/apps/api/node_modules/.bin/tsc -p /run/user/1002/orca-claude/claude-1002/-home-dev/a31610d6-d491-4616-9c73-f94008cd6a0f/scratchpad/mun0046-graph/verifier-out/tsconfig/apps-api-apps-api-tsconfig-test-json.json --noEmit --incremental false --listFiles","entities":["code_unit:apps/api/test/app-module-boot.e2e.spec.ts","code_unit:apps/api/test/field-changes.e2e.spec.ts","code_unit:apps/api/test/tasks-agent-scope.e2e.spec.ts"],"exit_code":0,"id":"v-type-check-apps-api-tsconfig-test","kind":"type_check","output_ref":"/run/user/1002/orca-claude/claude-1002/-home-dev/a31610d6-d491-4616-9c73-f94008cd6a0f/scratchpad/mun0046-graph/verifier-out/v-type-check-apps-api-tsconfig-test.txt","summary":"apps/api/tsconfig.test.json: exit 0, 0 error(s), 1457 files listed"},{"command":"/home/dev/aup/muneral-mun0046/apps/web/node_modules/.bin/tsc -p /run/user/1002/orca-claude/claude-1002/-home-dev/a31610d6-d491-4616-9c73-f94008cd6a0f/scratchpad/mun0046-graph/verifier-out/tsconfig/apps-web-apps-web-tsconfig-json.json --noEmit --incremental false --listFiles","entities":["code_unit:apps/web/__tests__/kanban-board.test.ts","code_unit:apps/web/app/(app)/workspaces/[wsSlug]/dashboard/page.tsx","code_unit:apps/web/app/(app)/workspaces/[wsSlug]/projects/[projSlug]/page.tsx","code_unit:apps/web/app/(app)/workspaces/[wsSlug]/projects/[projSlug]/tasks/[taskId]/page.tsx","code_unit:apps/web/components/dashboard/BlockersList.tsx","code_unit:apps/web/components/kanban/KanbanBoard.tsx","code_unit:apps/web/components/kanban/KanbanCard.tsx","code_unit:apps/web/components/kanban/KanbanColumn.tsx","code_unit:apps/web/components/task/AuditLog.tsx","code_unit:apps/web/components/task/TaskChecklist.tsx","code_unit:apps/web/components/task/TaskDependencies.tsx","code_unit:apps/web/components/task/TaskDetail.tsx","code_unit:apps/web/lib/api/tasks.ts","deployable_unit:apps/web"],"exit_code":0,"id":"v-type-check-apps-web-tsconfig","kind":"type_check","output_ref":"/run/user/1002/orca-claude/claude-1002/-home-dev/a31610d6-d491-4616-9c73-f94008cd6a0f/scratchpad/mun0046-graph/verifier-out/v-type-check-apps-web-tsconfig.txt","summary":"apps/web/tsconfig.json: exit 0, 0 error(s), 1074 files listed"},{"command":"tools/graph/contract_diff.py.run_diff(base=cd64f98f2bb1, head=5d22b094d1b4, graph=head graph)","entities":["code_unit:apps/api/src/tasks/tasks.controller.ts","code_unit:apps/web/__tests__/kanban-board.test.ts","code_unit:apps/web/app/(app)/workspaces/[wsSlug]/dashboard/page.tsx","code_unit:apps/web/app/(app)/workspaces/[wsSlug]/projects/[projSlug]/page.tsx","code_unit:apps/web/app/(app)/workspaces/[wsSlug]/projects/[projSlug]/tasks/[taskId]/page.tsx","code_unit:apps/web/components/dashboard/BlockersList.tsx","code_unit:apps/web/components/kanban/KanbanBoard.tsx","code_unit:apps/web/components/kanban/KanbanCard.tsx","code_unit:apps/web/components/kanban/KanbanColumn.tsx","code_unit:apps/web/components/task/AuditLog.tsx","code_unit:apps/web/components/task/TaskChecklist.tsx","code_unit:apps/web/components/task/TaskDependencies.tsx","code_unit:apps/web/components/task/TaskDetail.tsx","code_unit:apps/web/lib/api/tasks.ts","route:DELETE /tasks/:taskId","route:DELETE /tasks/:taskId/checklist/:itemId","route:DELETE /tasks/:taskId/dependencies/:depId","route:GET /tasks/:taskId","route:GET /tasks/:taskId/activity","route:GET /tasks/:taskId/checklist","route:GET /tasks/:taskId/dependencies","route:GET /tasks/project/:projectId","route:PATCH /tasks/:taskId/checklist/:itemId","route:PATCH /tasks/:taskId/status","route:POST /tasks","route:POST /tasks/:taskId/checklist","route:POST /tasks/:taskId/comments","route:POST /tasks/:taskId/dependencies"],"exit_code":0,"id":"v-contract-diff","kind":"contract_diff","output_ref":"/run/user/1002/orca-claude/claude-1002/-home-dev/a31610d6-d491-4616-9c73-f94008cd6a0f/scratchpad/mun0046-graph/verifier-out/v-contract-diff.json","summary":"39→39 contracts, breaking 0, edges {'verified': 126}"},{"command":"verify.py route_config_consistency RC-01 prefix / RC-02 registration / RC-03 served","entities":["route:DELETE /tasks/:taskId","route:DELETE /tasks/:taskId/checklist/:itemId","route:DELETE /tasks/:taskId/dependencies/:depId","route:GET /tasks/:taskId","route:GET /tasks/:taskId/activity","route:GET /tasks/:taskId/checklist","route:GET /tasks/:taskId/dependencies","route:GET /tasks/project/:projectId","route:PATCH /tasks/:taskId/checklist/:itemId","route:PATCH /tasks/:taskId/status","route:POST /tasks","route:POST /tasks/:taskId/checklist","route:POST /tasks/:taskId/comments","route:POST /tasks/:taskId/dependencies"],"exit_code":0,"id":"v-route-config","kind":"config_schema","output_ref":"/run/user/1002/orca-claude/claude-1002/-home-dev/a31610d6-d491-4616-9c73-f94008cd6a0f/scratchpad/mun0046-graph/verifier-out/v-route-config.txt","summary":"14 route(s), 0 failed"},{"command":"verify.py fitness_rules FR-01..FR-05 over the head graph + head sources","entities":["code_unit:apps/api/src/app.module.ts","code_unit:apps/api/src/main.ts","code_unit:apps/api/src/tasks/tasks.controller.ts","code_unit:apps/api/src/tasks/tasks.module.ts","code_unit:apps/api/test/app-module-boot.e2e.spec.ts","code_unit:apps/api/test/field-changes.e2e.spec.ts","code_unit:apps/api/test/tasks-agent-scope.e2e.spec.ts","code_unit:apps/web/__tests__/kanban-board.test.ts","code_unit:apps/web/app/(app)/workspaces/[wsSlug]/dashboard/page.tsx","code_unit:apps/web/app/(app)/workspaces/[wsSlug]/projects/[projSlug]/page.tsx","code_unit:apps/web/app/(app)/workspaces/[wsSlug]/projects/[projSlug]/tasks/[taskId]/page.tsx","code_unit:apps/web/components/dashboard/BlockersList.tsx","code_unit:apps/web/components/kanban/KanbanBoard.tsx","code_unit:apps/web/components/kanban/KanbanCard.tsx","code_unit:apps/web/components/kanban/KanbanColumn.tsx","code_unit:apps/web/components/task/AuditLog.tsx","code_unit:apps/web/components/task/TaskChecklist.tsx","code_unit:apps/web/components/task/TaskDependencies.tsx","code_unit:apps/web/components/task/TaskDetail.tsx","code_unit:apps/web/lib/api/tasks.ts","deployable_unit:apps/api","deployable_unit:apps/web"],"exit_code":0,"id":"v-fitness","kind":"fitness_rules","output_ref":"/run/user/1002/orca-claude/claude-1002/-home-dev/a31610d6-d491-4616-9c73-f94008cd6a0f/scratchpad/mun0046-graph/verifier-out/v-fitness.json","summary":"5 finding(s): 0 new / 5 frozen; 0 entity(ies) failed"},{"command":"verify.py doc_reference: path / route / model references of affected documents resolve at head","entities":["document:README.md","document:apps/api/docs/agent-read-loop.md","document:apps/api/docs/migration-import.md"],"exit_code":0,"id":"v-doc-reference","kind":"doc_reference","output_ref":"/run/user/1002/orca-claude/claude-1002/-home-dev/a31610d6-d491-4616-9c73-f94008cd6a0f/scratchpad/mun0046-graph/verifier-out/v-doc-reference.txt","summary":"3 document(s), 0 failed"},{"command":"tools/graph/deploy_gate.py canary --plan <plan> --phase pre|post (results supplied with --canary)","entities":["code_unit:apps/api/src/tasks/tasks.controller.ts","code_unit:apps/web/__tests__/kanban-board.test.ts","code_unit:apps/web/app/(app)/workspaces/[wsSlug]/dashboard/page.tsx","code_unit:apps/web/app/(app)/workspaces/[wsSlug]/projects/[projSlug]/page.tsx","code_unit:apps/web/app/(app)/workspaces/[wsSlug]/projects/[projSlug]/tasks/[taskId]/page.tsx","code_unit:apps/web/components/dashboard/BlockersList.tsx","code_unit:apps/web/components/kanban/KanbanBoard.tsx","code_unit:apps/web/components/kanban/KanbanCard.tsx","code_unit:apps/web/components/kanban/KanbanColumn.tsx","code_unit:apps/web/components/task/AuditLog.tsx","code_unit:apps/web/components/task/TaskChecklist.tsx","code_unit:apps/web/components/task/TaskDependencies.tsx","code_unit:apps/web/components/task/TaskDetail.tsx","code_unit:apps/web/lib/api/tasks.ts","deployable_unit:apps/web","route:DELETE /tasks/:taskId","route:DELETE /tasks/:taskId/checklist/:itemId","route:DELETE /tasks/:taskId/dependencies/:depId","route:GET /tasks/:taskId","route:GET /tasks/:taskId/activity","route:GET /tasks/:taskId/checklist","route:GET /tasks/:taskId/dependencies","route:GET /tasks/project/:projectId","route:PATCH /tasks/:taskId/checklist/:itemId","route:PATCH /tasks/:taskId/status","route:POST /tasks","route:POST /tasks/:taskId/checklist","route:POST /tasks/:taskId/comments","route:POST /tasks/:taskId/dependencies"],"exit_code":0,"id":"v-canary","kind":"canary","output_ref":"/run/user/1002/orca-claude/claude-1002/-home-dev/a31610d6-d491-4616-9c73-f94008cd6a0f/scratchpad/mun0046-graph/verifier-out/v-canary.json","summary":"0 canary result(s); 0 verified / 0 failed / 29 not_measured of 29 entities"},{"command":"/home/dev/aup/muneral-mun0046/apps/api/node_modules/.bin/jest --runInBand --ci test/app-module-boot.e2e.spec.ts test/field-changes.e2e.spec.ts test/tasks-agent-scope.e2e.spec.ts","entities":["code_unit:apps/api/src/tasks/tasks.module.ts","code_unit:apps/api/test/app-module-boot.e2e.spec.ts","code_unit:apps/api/test/field-changes.e2e.spec.ts","code_unit:apps/api/test/tasks-agent-scope.e2e.spec.ts"],"exit_code":0,"id":"v-targeted-test-apps-api","kind":"targeted_test","output_ref":"/run/user/1002/orca-claude/claude-1002/-home-dev/a31610d6-d491-4616-9c73-f94008cd6a0f/scratchpad/mun0046-graph/verifier-out/v-targeted-test-apps-api.txt","summary":"3 spec(s), exit 0, 0 failed"},{"command":"/home/dev/aup/muneral-mun0046/apps/web/node_modules/.bin/vitest run __tests__/kanban-board.test.ts","entities":["code_unit:apps/web/__tests__/kanban-board.test.ts","code_unit:apps/web/lib/api/tasks.ts"],"exit_code":0,"id":"v-targeted-test-apps-web","kind":"targeted_test","output_ref":"/run/user/1002/orca-claude/claude-1002/-home-dev/a31610d6-d491-4616-9c73-f94008cd6a0f/scratchpad/mun0046-graph/verifier-out/v-targeted-test-apps-web.txt","summary":"1 spec(s), exit 0, 0 failed"}],"work_item":"MUN-0046a","model":"claude-sonnet-5","provisional_until_fable_review":true,"model_routing_decision":"DEC-AUP-0015"}

🤖 Generated with Claude Code

https://claude.ai/code/session_01W3m81Wh6zd7c2dZpPhxu95

…comments

The comments route carried no @AgentScope decorator, so AgentTaskScopeGuard's
default-deny refused every mun_sk_ agent key with 403 -- the same shape
MUN-0045 fixed on POST /tasks, one route over (measured live in
receipts/muneral/mac-e30-tasks-verified-20260907T013920Z.json, program repo).

Reuses the existing 'task' AgentScopeKind (no new kind needed): a comment is
an act on one specific task the agent must already be assigned to, same as
GET /tasks/:taskId and PATCH /tasks/:taskId/status. Authorship unaffected --
AddCommentDto carries only `body`; the stored actor comes from req.actor
(ActorInterceptor), never the request body.

Negative control and abuse case both added and green (tasks-agent-scope.e2e.spec.ts):
an unassigned agent (same or foreign workspace) still 403s, 0 comments written;
a forged actorId/actorType/actor in the body is stripped by the global
ValidationPipe and never honoured.

Regenerated apps/api/test/assembly/mutation-results.json against this tree
(85/85 sites, 0 survived) -- unrelated subsystem, tree-hash-bound evidence,
same mechanism MUN-0044/MUN-0045 documented.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01W3m81Wh6zd7c2dZpPhxu95
@Arcanada
Arcanada merged commit 8d52c2c into main Sep 7, 2026
8 checks passed
Arcanada added a commit that referenced this pull request Sep 14, 2026
…ESM move dropped

Dependabot #103 proposes node 24.20.0 -> 26.8-alpine. Measured against
nodejs.org/dist/index.json: the 24.x line is the CURRENT LTS (Krypton, v24.21.0
released 2026-09-07) and every 26.x release carries `lts: false` — 26 is Current,
not LTS. Moving production off a supported line is not a dependency bump, so this
takes the security patch WITHIN the LTS line instead: 24.21.0 everywhere.

There is a concrete reason to take it. v24.21.0 fixes an out-of-bounds write in
`mkdtemp` for long prefixes (nodejs/node#64770), and this repository calls
`fs.mkdtempSync` in three places, one of them the evidence path itself
(mutation-harness.js gitSupplement). Five pins move together, because
regenerate-derived-artefacts.yml REFUSES a mismatch between ci.yml and
.github/workflows/.node-version-for-derived: apps/api/Dockerfile (x2), ci.yml,
that pin file, and the root engines field.

THE DEFENCE THAT WAS LOST. Two defects, both introduced by the ESM move in #113
and both invisible until a full mutation run was attempted:

1. The harness spawns jest directly and did not pass --experimental-vm-modules,
   so under ESM every battery failed with `SyntaxError: Cannot use import
   statement outside a module` and the run stopped at `baseline: FAIL`. Measured:
   20 suites / 394 tests pass with the flag, 20 suites fail with 0 tests without
   it. `pnpm test` sets the flag; the harness never did. It went unnoticed because
   the only caller of a full run is regenerate-derived-artefacts.yml, gated on
   author == 'dependabot[bot]' — it never ran for #113. NODE_OPTIONS is appended,
   not assigned, so an inherited flag survives.

2. `isolatedModules: true` on the ts-jest transform removes type-checking at
   transform time, and nine of the 85 mutants were KILLED_BY_TYPECHECK. Measured
   with the flag: 79 killed by test, 6 SURVIVED — #11, #15, #16, #50, #52, #75,
   each a mutant the compiler used to reject and the tests do not catch. Three of
   the nine (#6, #13, #21) turned out to be caught by tests after all; six were
   not. A mutation baseline that records 0 survivors while the tree actually has 6
   is worse than no baseline. The flag is removed.

Its cost, measured: the local suite goes from 26s to 58s, and the CI job from
about 3 minutes to an expected 3.5-4. Six mutants of the canonical-JSON and
validator paths are worth thirty seconds.

Measured after, from the repository root (the invocation the evidence records):
85 sites, 76 killed by test, 9 killed by compiler, 0 survived — identical to the
counts on main. `--verify-structure` prints "Structurally verified 85
source-bound mutation records" with no TOOLCHAIN_MISMATCH, because tools now read
node v24.21.0. tsc --noEmit 0 under both tsconfig.json and tsconfig.test.json.

Closes #103 by answering it: the upgrade it asks for is refused with a measurement
and the security patch it was reaching for is taken.
Arcanada added a commit that referenced this pull request Sep 14, 2026
…ESM move dropped (#114)

* fix: stay on the Node LTS line, and restore the mutation defence the ESM move dropped

Dependabot #103 proposes node 24.20.0 -> 26.8-alpine. Measured against
nodejs.org/dist/index.json: the 24.x line is the CURRENT LTS (Krypton, v24.21.0
released 2026-09-07) and every 26.x release carries `lts: false` — 26 is Current,
not LTS. Moving production off a supported line is not a dependency bump, so this
takes the security patch WITHIN the LTS line instead: 24.21.0 everywhere.

There is a concrete reason to take it. v24.21.0 fixes an out-of-bounds write in
`mkdtemp` for long prefixes (nodejs/node#64770), and this repository calls
`fs.mkdtempSync` in three places, one of them the evidence path itself
(mutation-harness.js gitSupplement). Five pins move together, because
regenerate-derived-artefacts.yml REFUSES a mismatch between ci.yml and
.github/workflows/.node-version-for-derived: apps/api/Dockerfile (x2), ci.yml,
that pin file, and the root engines field.

THE DEFENCE THAT WAS LOST. Two defects, both introduced by the ESM move in #113
and both invisible until a full mutation run was attempted:

1. The harness spawns jest directly and did not pass --experimental-vm-modules,
   so under ESM every battery failed with `SyntaxError: Cannot use import
   statement outside a module` and the run stopped at `baseline: FAIL`. Measured:
   20 suites / 394 tests pass with the flag, 20 suites fail with 0 tests without
   it. `pnpm test` sets the flag; the harness never did. It went unnoticed because
   the only caller of a full run is regenerate-derived-artefacts.yml, gated on
   author == 'dependabot[bot]' — it never ran for #113. NODE_OPTIONS is appended,
   not assigned, so an inherited flag survives.

2. `isolatedModules: true` on the ts-jest transform removes type-checking at
   transform time, and nine of the 85 mutants were KILLED_BY_TYPECHECK. Measured
   with the flag: 79 killed by test, 6 SURVIVED — #11, #15, #16, #50, #52, #75,
   each a mutant the compiler used to reject and the tests do not catch. Three of
   the nine (#6, #13, #21) turned out to be caught by tests after all; six were
   not. A mutation baseline that records 0 survivors while the tree actually has 6
   is worse than no baseline. The flag is removed.

Its cost, measured: the local suite goes from 26s to 58s, and the CI job from
about 3 minutes to an expected 3.5-4. Six mutants of the canonical-JSON and
validator paths are worth thirty seconds.

Measured after, from the repository root (the invocation the evidence records):
85 sites, 76 killed by test, 9 killed by compiler, 0 survived — identical to the
counts on main. `--verify-structure` prints "Structurally verified 85
source-bound mutation records" with no TOOLCHAIN_MISMATCH, because tools now read
node v24.21.0. tsc --noEmit 0 under both tsconfig.json and tsconfig.test.json.

Closes #103 by answering it: the upgrade it asks for is refused with a measurement
and the security patch it was reaching for is taken.

* receipt(#103): file the ChangeAdmissionReceipt/v1 for the Node LTS pin

Draft by the bundle's own verify.py in diff mode: verdict admitted, 3 entities,
all three verified, zero exemptions — nothing needed exempting, which is the
first receipt in this series that can say so.

One finding came out of it and was fixed rather than exempted. The first draft
reported v-config-schema UNDECLARED_CONFIG_KEY NODE_OPTIONS as failed, because
the harness fix read process.env.NODE_OPTIONS to append to an inherited value.
The verifier was right: reading it makes it a configuration key this file
consumes, and declaring a test-harness runtime flag in .env.example — the
application's own configuration — would have been the worse answer. The flag is
now set outright, and regenerate-derived-artefacts.yml (the only caller of a full
run) sets no NODE_OPTIONS, so nothing is lost.

Filed in the tree with the receipt included in its own change_set, and the
evidence's trackedTreeWithoutEvidence rebound after staging it — the MUN-0049
precedent. schema_check.py: CONFORMANT.
@Arcanada
Arcanada deleted the mun-0046a-comments-scope branch November 14, 2026 02:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant