@howaboua/pi-pet 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/LICENSE +21 -0
  3. package/README.md +92 -0
  4. package/SECURITY.md +24 -0
  5. package/authoring/PET-GUIDE.md +65 -0
  6. package/authoring/hatch/HATCH-GUIDE.md +136 -0
  7. package/authoring/hatch/LICENSE.txt +201 -0
  8. package/authoring/hatch/NOTICE.md +5 -0
  9. package/authoring/hatch/pyproject.toml +8 -0
  10. package/authoring/hatch/references/animation-rows.md +29 -0
  11. package/authoring/hatch/references/final-assembly.md +67 -0
  12. package/authoring/hatch/references/look-directions.md +35 -0
  13. package/authoring/hatch/references/pet-contract.md +40 -0
  14. package/authoring/hatch/references/qa-rubric.md +34 -0
  15. package/authoring/hatch/references/reviewer-protocol.md +72 -0
  16. package/authoring/hatch/references/standard-assembly.md +21 -0
  17. package/authoring/hatch/references/visual-generation.md +59 -0
  18. package/authoring/hatch/scripts/approve_cardinals.py +110 -0
  19. package/authoring/hatch/scripts/assemble_extended_atlas.py +684 -0
  20. package/authoring/hatch/scripts/combine_direction_blind_verdicts.py +59 -0
  21. package/authoring/hatch/scripts/compose_atlas.py +171 -0
  22. package/authoring/hatch/scripts/compose_cardinal_anchor_strip.py +45 -0
  23. package/authoring/hatch/scripts/derive_running_left_from_running_right.py +164 -0
  24. package/authoring/hatch/scripts/despill_chroma_edges.py +312 -0
  25. package/authoring/hatch/scripts/extract_cardinal_anchors.py +153 -0
  26. package/authoring/hatch/scripts/extract_strip_frames.py +409 -0
  27. package/authoring/hatch/scripts/inspect_frames.py +278 -0
  28. package/authoring/hatch/scripts/make_contact_sheet.py +116 -0
  29. package/authoring/hatch/scripts/make_direction_blind_qa_sheet.py +179 -0
  30. package/authoring/hatch/scripts/make_direction_qa_sheet.py +172 -0
  31. package/authoring/hatch/scripts/measure_direction_continuity.py +201 -0
  32. package/authoring/hatch/scripts/prepare_pet_run.py +1207 -0
  33. package/authoring/hatch/scripts/render_animation_previews.py +80 -0
  34. package/authoring/hatch/scripts/validate_atlas.py +299 -0
  35. package/authoring/hatch/scripts/validate_direction_blind_verdicts.py +100 -0
  36. package/authoring/hatch/uv.lock +99 -0
  37. package/authoring/references/import-existing-pet.md +47 -0
  38. package/authoring/references/pet-format.md +88 -0
  39. package/changelog.ts +327 -0
  40. package/deploy/pi-pet-hyprland.conf +10 -0
  41. package/desktop/build.mjs +194 -0
  42. package/desktop/package-lock.json +887 -0
  43. package/desktop/package.json +16 -0
  44. package/dist/authoring/prepare.mjs +3 -0
  45. package/dist/authoring/rebuild.mjs +4 -0
  46. package/dist/authoring/validate.mjs +2 -0
  47. package/dist/desktop/main.mjs +5 -0
  48. package/dist/desktop/preload.cjs +1 -0
  49. package/dist/web/app.js +1 -0
  50. package/dist/web/catalog.json +1 -0
  51. package/dist/web/index.html +100 -0
  52. package/dist/web/manifest.webmanifest +16 -0
  53. package/dist/web/pet-icon.svg +6 -0
  54. package/dist/web/spritesheet.webp +0 -0
  55. package/dist/web/styles.css +592 -0
  56. package/docs/architecture.md +41 -0
  57. package/docs/pi-pet.png +0 -0
  58. package/docs/research.md +17 -0
  59. package/extensions/desktop-command.ts +249 -0
  60. package/extensions/desktop-config.ts +152 -0
  61. package/extensions/desktop-launcher.ts +380 -0
  62. package/extensions/gippity.ts +24 -0
  63. package/extensions/index.ts +110 -0
  64. package/extensions/remote-app.ts +61 -0
  65. package/package.json +101 -0
  66. package/pets/clawa/PET.md +27 -0
  67. package/pets/clawa/pet.json +7 -0
  68. package/pets/clawa/spritesheet.webp +0 -0
  69. package/pets/clawa/validation.json +808 -0
  70. package/src/desktop/attention.ts +87 -0
  71. package/src/desktop/bridge.ts +31 -0
  72. package/src/desktop/config.ts +145 -0
  73. package/src/desktop/cursor-provider.ts +150 -0
  74. package/src/desktop/main.ts +459 -0
  75. package/src/desktop/preload.ts +19 -0
  76. package/src/pet-image-size.ts +72 -0
  77. package/src/pet-loader.ts +278 -0
  78. package/src/pet-storage.ts +240 -0
  79. package/src/protocol/index.ts +81 -0
@@ -0,0 +1,67 @@
1
+ # Final assembly
2
+
3
+ Read `chroma_key.hex` from the run's `pet_request.json` with an available JSON capability and substitute that literal value for `<chroma-key>` below.
4
+
5
+ Register row 9 before generating row 10:
6
+
7
+ ```bash
8
+ uv run --project "$HATCH_DIR" --locked python "$HATCH_DIR/scripts/assemble_extended_atlas.py" \
9
+ --base-atlas "$RUN_DIR/final/spritesheet.webp" --look-row-9 "$RUN_DIR/decoded/look-row-9.png" \
10
+ --neutral-cell "$RUN_DIR/frames/idle/00.png" --chroma-key "<chroma-key>" \
11
+ --registered-row-output "$RUN_DIR/qa/look-row-9-registered.png" \
12
+ --registration-manifest-output "$RUN_DIR/qa/look-row-9-registration.json"
13
+ ```
14
+
15
+ Assemble row 10 against the approved row-9 transform:
16
+
17
+ ```bash
18
+ uv run --project "$HATCH_DIR" --locked python "$HATCH_DIR/scripts/assemble_extended_atlas.py" \
19
+ --base-atlas "$RUN_DIR/final/spritesheet.webp" \
20
+ --registered-row-9 "$RUN_DIR/qa/look-row-9-registered.png" \
21
+ --row-9-registration "$RUN_DIR/qa/look-row-9-registration.json" \
22
+ --look-row-10 "$RUN_DIR/decoded/look-row-10.png" \
23
+ --neutral-cell "$RUN_DIR/frames/idle/00.png" --chroma-key "<chroma-key>" \
24
+ --output "$RUN_DIR/final/spritesheet-extended.png" \
25
+ --webp-output "$RUN_DIR/final/spritesheet-extended.webp" \
26
+ --manifest-output "$RUN_DIR/final/spritesheet-extended.json"
27
+ ```
28
+
29
+ Run one—and only one—despill pass, then validate:
30
+
31
+ ```bash
32
+ uv run --project "$HATCH_DIR" --locked python "$HATCH_DIR/scripts/despill_chroma_edges.py" \
33
+ "$RUN_DIR/final/spritesheet-extended.png" --output "$RUN_DIR/final/spritesheet-extended.png" \
34
+ --webp-output "$RUN_DIR/final/spritesheet-extended.webp" --chroma-key "<chroma-key>" \
35
+ --json-out "$RUN_DIR/qa/chroma-despill-extended.json"
36
+ uv run --project "$HATCH_DIR" --locked python "$HATCH_DIR/scripts/validate_atlas.py" \
37
+ "$RUN_DIR/final/spritesheet-extended.webp" \
38
+ --json-out "$RUN_DIR/final/validation-extended.json" --chroma-key "<chroma-key>" --require-v2
39
+ ```
40
+
41
+ Produce QA media:
42
+
43
+ ```bash
44
+ uv run --project "$HATCH_DIR" --locked python "$HATCH_DIR/scripts/make_contact_sheet.py" \
45
+ "$RUN_DIR/final/spritesheet-extended.webp" --output "$RUN_DIR/qa/contact-sheet-extended.png"
46
+ uv run --project "$HATCH_DIR" --locked python "$HATCH_DIR/scripts/make_direction_qa_sheet.py" \
47
+ "$RUN_DIR/final/spritesheet-extended.webp" --output "$RUN_DIR/qa/look-directions.png"
48
+ uv run --project "$HATCH_DIR" --locked python "$HATCH_DIR/scripts/make_direction_blind_qa_sheet.py" \
49
+ "$RUN_DIR/final/spritesheet-extended.webp" --output "$RUN_DIR/qa/direction-blind-pairs.png" \
50
+ --answer-key "$RUN_DIR/qa/direction-blind-answer-key.json"
51
+ uv run --project "$HATCH_DIR" --locked python "$HATCH_DIR/scripts/measure_direction_continuity.py" \
52
+ "$RUN_DIR/final/spritesheet-extended.webp" --json-out "$RUN_DIR/qa/look-continuity.json"
53
+ ```
54
+
55
+ After three isolated verdict files exist:
56
+
57
+ ```bash
58
+ uv run --project "$HATCH_DIR" --locked python "$HATCH_DIR/scripts/combine_direction_blind_verdicts.py" \
59
+ --verdicts "$RUN_DIR/qa/direction-blind-verdicts-1.json" \
60
+ --verdicts "$RUN_DIR/qa/direction-blind-verdicts-2.json" \
61
+ --verdicts "$RUN_DIR/qa/direction-blind-verdicts-3.json" \
62
+ --json-out "$RUN_DIR/qa/direction-blind-verdicts.json"
63
+ uv run --project "$HATCH_DIR" --locked python "$HATCH_DIR/scripts/validate_direction_blind_verdicts.py" \
64
+ --answer-key "$RUN_DIR/qa/direction-blind-answer-key.json" \
65
+ --verdicts "$RUN_DIR/qa/direction-blind-verdicts.json" \
66
+ --json-out "$RUN_DIR/qa/direction-blind-validation.json"
67
+ ```
@@ -0,0 +1,35 @@
1
+ # Look-direction generation
2
+
3
+ ## Mechanics plan
4
+
5
+ Write `qa/look-mechanics.md` before generation:
6
+
7
+ - natural gaze mechanism for this construction;
8
+ - stable anchor and baseline;
9
+ - eyes/eyelids/head/body parts that lead and follow;
10
+ - deformation allowed by material;
11
+ - prop attachment, occlusion, and lag;
12
+ - cardinal pose families and expected visible sides;
13
+ - roughly even motion budget for each 22.5° step.
14
+
15
+ Physical eyeballs rotate as complete globes with iris, pupil, eyelids, rim, and highlights. Printed/screen eyes may move features on a fixed surface. Flexible bodies bend; blobs stretch subtly; separate heads turn; rigid objects use an appropriate hinge, aim, lean, or attached feature. Preserve the original eye design—never add replacement or googly eyes.
16
+
17
+ ## Cardinal gate
18
+
19
+ Generate `000 up`, `090 screen-right`, `180 down`, and `270 screen-left together. Viewer/screen coordinates are authoritative. At normal pet size, cardinals must be unmistakable from pupils, nose, face surface, head/body direction, or the pet's natural aiming feature. Regenerate an ambiguous anchor.
20
+
21
+ ## Coherent rows
22
+
23
+ - Row 9: `000, 022.5, 045, 067.5, 090, 112.5, 135, 157.5`.
24
+ - Row 10: `180, 202.5, 225, 247.5, 270, 292.5, 315, 337.5`.
25
+ - Generate each row as one eight-pose family, not eight unrelated cells.
26
+ - Keep scale, lower-body anchor, baseline, identity, and props continuous.
27
+ - Row 10 receives completed row 9; `157.5→180` and `337.5→000` are normal adjacent transitions.
28
+ - Every look pose differs visibly from neutral at actual display size.
29
+ - No whole-sprite rotation, skew, affine tilt, procedural pupil overlay, independent recentering, or patched final cell unless the pet's literal construction and mechanics plan justify it.
30
+
31
+ ## Acceptance
32
+
33
+ Hard failure: wrong/ambiguous cardinal, labeled wrong quadrant, reversal, conspicuous snap, scale/registration pop, identity change, broken attachment, clipping, seam, transparent interior damage, or replacement eyes.
34
+
35
+ Review warning: subtle intermediate cue, similar neighbors, isolated blind uncertainty, or a metric outlier without a visible defect. Warnings require recorded labeled-loop judgment; they do not automatically force regeneration.
@@ -0,0 +1,40 @@
1
+ # Pi Pet v2 contract
2
+
3
+ ## Package
4
+
5
+ ```text
6
+ pets/<pet-id>/
7
+ ├── PET.md
8
+ ├── pet.json
9
+ ├── spritesheet.webp
10
+ └── validation.json
11
+ ```
12
+
13
+ `pet.json`:
14
+
15
+ ```json
16
+ {
17
+ "id": "pet-id",
18
+ "displayName": "Pet Name",
19
+ "description": "One short sentence.",
20
+ "spriteVersionNumber": 2,
21
+ "spritesheetPath": "spritesheet.webp"
22
+ }
23
+ ```
24
+
25
+ Pi Pet deliberately consumes the Codex-compatible v2 base format. An optional `pet.pi.json` extension layer may add later Pi-only actions; it is not part of hatching the standard atlas.
26
+
27
+ ## Atlas
28
+
29
+ - PNG or WebP, exactly `1536×2288`.
30
+ - 8 columns × 11 rows; each cell `192×208`.
31
+ - Rows 0–8 are standard animations. Rows 9–10 are sixteen clockwise look directions.
32
+ - Used cells contain visible pixels. Unused standard-row cells are fully transparent.
33
+ - Fully transparent pixels have zero RGB residue. No background, guide marks, seams, clipping, or colored alpha fringe.
34
+ - The intermediate 8×9 `1536×1872` atlas is review-only and never packaged.
35
+
36
+ Coordinates are deterministic: `x = column × 192`, `y = row × 208`.
37
+
38
+ ## Data boundary
39
+
40
+ Packages are inert data. Asset paths stay relative, regular, bounded PNG/WebP files inside the package. Never add scripts, HTML, event handlers, remote URLs, absolute paths, traversal, or escaping symlinks.
@@ -0,0 +1,34 @@
1
+ # Acceptance rubric
2
+
3
+ Do not package until every required item passes.
4
+
5
+ ## Geometry and data
6
+
7
+ - Final atlas exactly `1536×2288`, 8×11 grid, `192×208` cells.
8
+ - `spriteVersionNumber: 2`; used cells visible; unused standard cells transparent.
9
+ - Fully transparent pixels have zero RGB residue.
10
+ - Final despill report and v2 validation have `ok: true`; frame review has no errors.
11
+ - Durable package under `$PET_DATA/pets/<pet-id>` contains inert bounded files only and passes `npm run pet:validate -- $PET_DATA/pets/<pet-id>` from the installed Pi Pet package root.
12
+
13
+ ## Identity and motion
14
+
15
+ - Same silhouette, proportions, face, expression language, material, palette, markings, lighting, and props across all rows.
16
+ - Every standard row has exact frame count and recognizable semantics.
17
+ - Idle is calm but not static; gait alternates; loops do not pop or reverse.
18
+ - Waiting, working, review, and failure remain visually distinct.
19
+ - No unintended character, object, logo, text, scene, shadow, detached effect, guide, or background.
20
+
21
+ ## Directions
22
+
23
+ - Four approved cardinals and all sixteen directions in fixed clockwise order.
24
+ - Coherent adjacent movement, stable baseline/scale/anchor, no whole-sprite rotation or replacement eyes.
25
+ - Focused direction sheet, blind consensus, labeled semantics, and continuity evidence exist.
26
+ - Cardinal blind gates pass. No labeled wrong quadrant, reversal, clipping, identity drift, or unexplained visible continuity defect remains.
27
+ - Any warning has explicit evidence and reviewer/user disposition; no `fail` remains.
28
+
29
+ ## Review and evidence
30
+
31
+ - Three isolated blind reviews plus independent final visual QA, or an explicitly recorded user-review fallback with reduced assurance.
32
+ - Repairs were independently re-reviewed.
33
+ - Retain request, final atlas, validation, despill, contact sheets, previews, direction sheets, blind verdicts/validation, semantics, continuity, final review, and run summary.
34
+ - `PET.md` records identity, action semantics, generation provenance, review method, and licensing uncertainty.
@@ -0,0 +1,72 @@
1
+ # Reviewer protocol
2
+
3
+ ## Isolation
4
+
5
+ - Use three fresh vision-capable reviewers for blind direction classification.
6
+ - Give each only `qa/direction-blind-pairs.png`; never provide labels, prompts, atlas order, answer key, prior verdicts, or another review.
7
+ - Use a separate final visual reviewer after deterministic processing and blind consensus.
8
+ - Reviewers inspect and report; the parent owns repairs, files, packaging, and cleanup.
9
+
10
+ ## Blind direction review
11
+
12
+ Prompt each reviewer:
13
+
14
+ ```text
15
+ Inspect only the supplied unlabeled A/B direction sheet.
16
+
17
+ Each row names one axis. For a horizontal row, classify A and B as screen-left, screen-right, or ambiguous. For a vertical row, classify A and B as up, down, or ambiguous. Judge at displayed pet size from pupils, nose, face/head turn, body aim, or the pet's natural aiming feature. Never infer from A/B order and do not force opposite answers.
18
+
19
+ Return exactly one JSON object:
20
+ {"pairs":[{"pair":"horizontal-1|vertical-1","A":"screen-left|screen-right|up|down|ambiguous","B":"screen-left|screen-right|up|down|ambiguous","reason":"short landmark evidence"}]}
21
+ Include every pair shown.
22
+ ```
23
+
24
+ Write responses separately as `direction-blind-verdicts-{1,2,3}.json`, combine by strict majority, then validate against the hidden key. Cardinal ambiguity or mismatch blocks packaging. Intermediate disagreement becomes labeled-review evidence.
25
+
26
+ ## Final visual review
27
+
28
+ Give the final reviewer:
29
+
30
+ - standard and extended contact sheets;
31
+ - all standard-row GIF previews;
32
+ - focused direction sheet;
33
+ - direction semantics, blind validation, and continuity JSON;
34
+ - frame review and v2 atlas validation.
35
+
36
+ Require exactly one JSON object and save it as `qa/final-visual-review.json`:
37
+
38
+ ```json
39
+ {
40
+ "visual_qa": "pass",
41
+ "qa_note": "One-sentence evidence summary.",
42
+ "reviewed_by": "agent:<identity>",
43
+ "directions": [
44
+ {
45
+ "direction": "000",
46
+ "expected": "up",
47
+ "verdict": "pass",
48
+ "horizontal": "neutral",
49
+ "vertical": "up",
50
+ "observed": "Visible landmark observation.",
51
+ "reason": "Why the evidence passes, warns, or fails."
52
+ }
53
+ ],
54
+ "warnings": [],
55
+ "repair_rows": [],
56
+ "repair_notes": []
57
+ }
58
+ ```
59
+
60
+ Require all sixteen directions in `directions`. Copy that array with `overall`, `reviewed_by`, and warnings to `qa/direction-semantics.json`; do not reduce it to prose.
61
+
62
+ The reviewer checks one identity/style across all rows, correct state semantics, complete motion, transparency, no clipping/overlap/guides/detached effects, stable scale/baseline, and a coherent clockwise look loop. Deterministic despill and atlas validation are authoritative for chroma; visual review must not invent another cleanup pass.
63
+
64
+ ## Failure resolution
65
+
66
+ - **Major:** wrong/ambiguous cardinal, labeled wrong quadrant/reversal, visible snap or scale pop, identity change, broken attachment, clipping, seam/hole, or deterministic failure. Repair required.
67
+ - **Minor:** subtle intermediate cue, reviewer disagreement, or metric warning without a visible defect. Parent or user may accept only with written labeled-loop evidence in `qa/blind-review-resolution.json`.
68
+ - Never override a major failure or let the repairing agent self-approve its repair.
69
+
70
+ ## Run summary
71
+
72
+ After acceptance, write `qa/run-summary.json` with `ok: true`, `spriteVersionNumber: 2`, review method, package path, and paths to the final atlas, validation, despill, both contact sheets, previews, direction sheet, semantics, blind validation, continuity, and final visual review. A missing artifact is a failed summary, not `null` evidence.
@@ -0,0 +1,21 @@
1
+ # Standard assembly
2
+
3
+ After all nine standard strips pass incremental checks:
4
+
5
+ ```bash
6
+ uv run --project "$HATCH_DIR" --locked python "$HATCH_DIR/scripts/extract_strip_frames.py" \
7
+ --decoded-dir "$RUN_DIR/decoded" --output-dir "$RUN_DIR/frames" --states all --method auto
8
+ uv run --project "$HATCH_DIR" --locked python "$HATCH_DIR/scripts/inspect_frames.py" \
9
+ --frames-root "$RUN_DIR/frames" --json-out "$RUN_DIR/qa/review.json" --require-components
10
+ uv run --project "$HATCH_DIR" --locked python "$HATCH_DIR/scripts/compose_atlas.py" \
11
+ --frames-root "$RUN_DIR/frames" --output "$RUN_DIR/final/spritesheet.png" \
12
+ --webp-output "$RUN_DIR/final/spritesheet.webp"
13
+ uv run --project "$HATCH_DIR" --locked python "$HATCH_DIR/scripts/make_contact_sheet.py" \
14
+ "$RUN_DIR/final/spritesheet.webp" --output "$RUN_DIR/qa/contact-sheet.png"
15
+ uv run --project "$HATCH_DIR" --locked python "$HATCH_DIR/scripts/render_animation_previews.py" \
16
+ --frames-root "$RUN_DIR/frames" --output-dir "$RUN_DIR/qa/previews"
17
+ ```
18
+
19
+ Inspect `qa/contact-sheet.png` and every GIF at actual pet scale. Reject identity/style drift, blank or clipped poses, copied guides, wrong state semantics, reversed or static gait, inert idle, baseline jumps, and size popping.
20
+
21
+ If `auto` extraction alone causes popping while the source strip has stable scale and placement, rerun extraction with `--method stable-slots`, then inspection with `--allow-stable-slots`, composition, contact sheet, and previews. Record the decision. Never use stable slots to hide a broken source strip.
@@ -0,0 +1,59 @@
1
+ # Visual generation handoff
2
+
3
+ ## Capability selection
4
+
5
+ A suitable capability can:
6
+
7
+ - generate or edit PNG/WebP from a supplied prompt;
8
+ - accept all reference images required by a job;
9
+ - preserve a canonical character across iterations;
10
+ - save the selected result to a known local path.
11
+
12
+ Do not require a named vendor, model, API, skill, or tool. Record the capability/service actually used in run evidence and `PET.md`.
13
+
14
+ ## Native generation
15
+
16
+ For each ready entry in `visual-jobs.json`:
17
+
18
+ 1. Open its `prompt_file`.
19
+ 2. Attach every `input_images` file with its recorded role.
20
+ 3. Generate only that job.
21
+ 4. Check frame count, identity, flat chroma background, spacing, clipping, and forbidden detached effects.
22
+ 5. Save the selected original, copy it to `output_path`, and record `source_path`, `qa_note`, and completion time atomically.
23
+
24
+ Use an available JSON capability to read the selected job's `output_path`, then an available file capability to create its parent and copy the selected original there. If those capabilities are unavailable, perform the same bounded JSON read and file copy with Python through the guide's locked uv environment. For the `base` job, also copy that output to `references/canonical-base.png`. Use the manifest-declared destinations; never invent parallel filenames.
25
+
26
+ After that job's required deterministic and visual checks pass, write `status: "complete"`, `source_path`, `qa_note`, and UTC `completed_at` through a temporary file plus atomic rename. Do not mark a copied but unreviewed strip complete.
27
+
28
+ One isolated worker per job prevents visual payloads and failed variants from flooding the parent context. Workers return only the selected path and a short evidence note.
29
+
30
+ ## No native generator
31
+
32
+ Do not declare the workflow blocked before preparing the run. Create `visual-jobs.json`, prompts, layout guides, and reference bundles first, then offer:
33
+
34
+ 1. **User-operated service:** ChatGPT image generation, Gemini, Grok Imagine, or any other service that supports prompts and reference uploads.
35
+ 2. **Authorized browser assistance:** when browser control exists, offer to upload references, submit the prepared prompt, and download the selected result through the user's already logged-in session.
36
+ 3. **Manual artwork:** accept files created by an artist or another application if they satisfy the same job contract.
37
+
38
+ For each external job, give the user:
39
+
40
+ - service-neutral prompt file;
41
+ - exact reference files and their roles;
42
+ - expected number/order of poses;
43
+ - required flat chroma color;
44
+ - destination filename;
45
+ - a warning not to generate the full atlas or include labels, grids, shadows, scenery, text, or guide marks.
46
+
47
+ After the file is returned, inspect and process it exactly like native output. External origin never weakens QA.
48
+
49
+ ## Browser boundary
50
+
51
+ Browser assistance requires the user's authorization for that service and upload. Use their existing session; never request or expose passwords, payment details, recovery codes, or session tokens. Do not purchase credits, accept new legal terms, publish images, or upload sensitive references without explicit approval. If download automation fails, ask the user to save the image and provide its path.
52
+
53
+ ## Grounding
54
+
55
+ - Only the base may be prompt-only.
56
+ - Standard rows use the canonical base and matching layout guide.
57
+ - Cardinals use the canonical base, approved standard contact sheet, and cardinal guide.
58
+ - Look row 9 uses approved cardinals; row 10 also uses completed row 9.
59
+ - Do not paste individual generations into a coherent look row or fake missing motion with deterministic transforms.
@@ -0,0 +1,110 @@
1
+ #!/usr/bin/env python3
2
+ """Complete the cardinal approval gate only when its required evidence exists."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ import os
9
+ from datetime import datetime, timezone
10
+ from pathlib import Path
11
+
12
+ from PIL import Image
13
+
14
+ MAX_MANIFEST_BYTES = 1024 * 1024
15
+ MAX_EVIDENCE_BYTES = 1024 * 1024
16
+ EXPECTED_STRIP_SIZE = (4 * 192, 208)
17
+
18
+
19
+ def load_object(path: Path, limit: int) -> dict[str, object]:
20
+ if not path.is_file() or path.stat().st_size > limit:
21
+ raise SystemExit(f"missing or oversized evidence: {path}")
22
+ try:
23
+ value = json.loads(path.read_text(encoding="utf-8"))
24
+ except (OSError, UnicodeError, json.JSONDecodeError) as error:
25
+ raise SystemExit(f"invalid JSON evidence: {path}: {error}") from error
26
+ if not isinstance(value, dict):
27
+ raise SystemExit(f"JSON evidence must be an object: {path}")
28
+ return value
29
+
30
+
31
+ def job(manifest: dict[str, object], job_id: str) -> dict[str, object]:
32
+ jobs = manifest.get("jobs")
33
+ if not isinstance(jobs, list):
34
+ raise SystemExit("visual-jobs.json jobs must be a list")
35
+ for value in jobs:
36
+ if isinstance(value, dict) and value.get("id") == job_id:
37
+ return value
38
+ raise SystemExit(f"visual-jobs.json is missing job: {job_id}")
39
+
40
+
41
+ def run_file(run_dir: Path, relative: str) -> Path:
42
+ if not relative or Path(relative).is_absolute() or ".." in Path(relative).parts:
43
+ raise SystemExit(f"approval artifact path is unsafe: {relative!r}")
44
+ path = (run_dir / relative).resolve()
45
+ try:
46
+ path.relative_to(run_dir)
47
+ except ValueError as error:
48
+ raise SystemExit(f"approval artifact escapes run directory: {relative}") from error
49
+ return path
50
+
51
+
52
+ def atomic_write(path: Path, value: dict[str, object]) -> None:
53
+ temporary = path.with_name(f".{path.name}.{os.getpid()}.tmp")
54
+ try:
55
+ temporary.write_text(json.dumps(value, indent=2) + "\n", encoding="utf-8")
56
+ temporary.replace(path)
57
+ finally:
58
+ temporary.unlink(missing_ok=True)
59
+
60
+
61
+ def main() -> None:
62
+ parser = argparse.ArgumentParser(description=__doc__)
63
+ parser.add_argument("--run-dir", required=True)
64
+ parser.add_argument("--reviewed-by", required=True)
65
+ parser.add_argument("--qa-note", required=True)
66
+ args = parser.parse_args()
67
+
68
+ reviewed_by = args.reviewed_by.strip()
69
+ qa_note = args.qa_note.strip()
70
+ if not reviewed_by or len(reviewed_by) > 80:
71
+ raise SystemExit("--reviewed-by must contain 1-80 characters")
72
+ if not qa_note or len(qa_note) > 500:
73
+ raise SystemExit("--qa-note must contain 1-500 characters")
74
+
75
+ run_dir = Path(args.run_dir).expanduser().resolve()
76
+ manifest_path = run_dir / "visual-jobs.json"
77
+ manifest = load_object(manifest_path, MAX_MANIFEST_BYTES)
78
+ source_job = job(manifest, "look-cardinals")
79
+ gate = job(manifest, "look-cardinals-approved")
80
+ if source_job.get("status") != "complete":
81
+ raise SystemExit("look-cardinals generation must be complete before approval")
82
+
83
+ required = gate.get("required_artifacts")
84
+ if not isinstance(required, list) or not all(isinstance(value, str) for value in required):
85
+ raise SystemExit("cardinal approval gate has invalid required_artifacts")
86
+ artifacts = {relative: run_file(run_dir, relative) for relative in required}
87
+ for relative, path in artifacts.items():
88
+ if not path.is_file() or path.stat().st_size <= 0 or path.stat().st_size > MAX_EVIDENCE_BYTES:
89
+ raise SystemExit(f"missing or oversized cardinal approval artifact: {relative}")
90
+
91
+ report = load_object(run_dir / "qa/cardinal-anchors.json", MAX_EVIDENCE_BYTES)
92
+ if report.get("ok") is not True:
93
+ raise SystemExit("cardinal anchor extraction did not pass")
94
+ with Image.open(run_dir / "decoded/look-anchors-approved.png") as image:
95
+ if image.size != EXPECTED_STRIP_SIZE:
96
+ raise SystemExit(
97
+ f"approved cardinal strip must be {EXPECTED_STRIP_SIZE[0]}x{EXPECTED_STRIP_SIZE[1]}"
98
+ )
99
+ image.verify()
100
+
101
+ gate["status"] = "complete"
102
+ gate["completed_at"] = datetime.now(timezone.utc).isoformat()
103
+ gate["reviewed_by"] = reviewed_by
104
+ gate["qa_note"] = qa_note
105
+ atomic_write(manifest_path, manifest)
106
+ print(json.dumps({"ok": True, "job_id": "look-cardinals-approved"}))
107
+
108
+
109
+ if __name__ == "__main__":
110
+ main()