From 7125ce939122b3226e10cbf364ba277876578791 Mon Sep 17 00:00:00 2001 From: kewton Date: Mon, 14 Sep 2026 13:56:34 +0900 Subject: [PATCH] Form contract-backed package script checks for #484 --- dev-reports/issue-484/design.md | 112 +++++ dev-reports/issue-484/formation-evidence.json | 456 ++++++++++++++++++ .../issue-484/implementation-summary.md | 85 ++++ dev-reports/issue-484/verification.md | 72 +++ src/minimal_loop/evidence.rs | 1 + .../evidence/verify_command_classification.rs | 6 +- .../package_script_check.rs | 48 ++ src/planner/lint.rs | 3 + src/planner/recovery_step_plan_binding.rs | 2 + .../recovery_step_plan_binding/admission.rs | 83 +++- .../formation_scope.rs | 17 +- .../issue466_tests.rs | 3 + .../issue484_capture_tests.rs | 70 +++ .../issue484_owner_tests.rs | 142 ++++++ .../issue484_runtime_tests.rs | 249 ++++++++++ .../issue484_tests.rs | 370 ++++++++++++++ .../package_owner_scope.rs | 146 ++++++ .../package_script_formation.rs | 342 +++++++++++++ .../verifier_formation.rs | 39 +- .../implementation_duties.rs | 22 +- src/planner/verify.rs | 26 +- src/planner/verify/setup_command_policy.rs | 27 ++ .../README.md | 37 ++ .../expectations.toml | 21 + .../formed-plan.json | 110 +++++ .../original-plan.json | 89 ++++ .../owner-refusals.json | 20 + .../package.json | 9 + .../refusals.json | 17 + .../saved-obligation-sources.json | 102 ++++ .../saved-scope.json | 88 ++++ .../source-manifest.json | 16 + .../unsupported-sources.json | 26 + 33 files changed, 2811 insertions(+), 45 deletions(-) create mode 100644 dev-reports/issue-484/design.md create mode 100644 dev-reports/issue-484/formation-evidence.json create mode 100644 dev-reports/issue-484/implementation-summary.md create mode 100644 dev-reports/issue-484/verification.md create mode 100644 src/minimal_loop/evidence/verify_command_classification/package_script_check.rs create mode 100644 src/planner/recovery_step_plan_binding/issue484_capture_tests.rs create mode 100644 src/planner/recovery_step_plan_binding/issue484_owner_tests.rs create mode 100644 src/planner/recovery_step_plan_binding/issue484_runtime_tests.rs create mode 100644 src/planner/recovery_step_plan_binding/issue484_tests.rs create mode 100644 src/planner/recovery_step_plan_binding/package_owner_scope.rs create mode 100644 src/planner/recovery_step_plan_binding/package_script_formation.rs create mode 100644 src/planner/verify/setup_command_policy.rs create mode 100644 tests/corpus/apps/issue484-package-script-formation/README.md create mode 100644 tests/corpus/apps/issue484-package-script-formation/expectations.toml create mode 100644 tests/corpus/apps/issue484-package-script-formation/formed-plan.json create mode 100644 tests/corpus/apps/issue484-package-script-formation/original-plan.json create mode 100644 tests/corpus/apps/issue484-package-script-formation/owner-refusals.json create mode 100644 tests/corpus/apps/issue484-package-script-formation/package.json create mode 100644 tests/corpus/apps/issue484-package-script-formation/refusals.json create mode 100644 tests/corpus/apps/issue484-package-script-formation/saved-obligation-sources.json create mode 100644 tests/corpus/apps/issue484-package-script-formation/saved-scope.json create mode 100644 tests/corpus/apps/issue484-package-script-formation/source-manifest.json create mode 100644 tests/corpus/apps/issue484-package-script-formation/unsupported-sources.json diff --git a/dev-reports/issue-484/design.md b/dev-reports/issue-484/design.md new file mode 100644 index 00000000..2d018ef5 --- /dev/null +++ b/dev-reports/issue-484/design.md @@ -0,0 +1,112 @@ +# Issue #484 design + +Baseline: `b7b65f8fd75793f2075491889c66372b02a06d14`, equal to fetched +`origin/develop`. The committed #478 model/host separation, #479 pure-import +formation and #480 executed failure predicates are present. + +Read before implementation: `docs/dev/dev-guardrails.md`, the approved +`20260914-issue484-485-content-review-01/issue-484-revised.md` and +`expected-value-source.json`, and the saved preclosure observation referenced +there. Historical evidence stays read-only. + +Implement a separate leaf for the closed `node -p` package-script grammar. +Accept only a root `require('./package.json').scripts.`. +Require an unambiguous fixed string in a saved executable package owner, using +the observed `Update package.json scripts so that dev is '…' and start is '…'.` +grammar. Parse the complete declaration, reject alternatives/conflicts, and +never derive a literal from app files, command output, or a later proposal. +The known host profile port constraint restricts a saved literal; it cannot +supply one. Retain both model and host duties without precedence. + +Capture source plans at the profile augmentation boundary, before sanitization, +and retain their source location, JSON pointers, owner, stage and SHA-256 hashes. +Store package expectations and the original registered contract on the first +formation failure. Proposed replacements must read the same target, compare +the exact string with `node:assert/strict`, and print the observed value after +the assertion. The closed whole-command grammar excludes wrappers, dynamic +properties, unrelated assertions, swallowed errors, and output suppression. +Project only validated replacements back to original commands for the existing +model/host scope and ordering checks. Record before/after commands, hashes, +expectation provenance and owners in additive event fields. + +Classify these executed comparisons as `StaticSyntax` (structural), preserving +the existing business-Test and implementation-artifact gates. Do not reopen +configured/frozen contracts, change recovery/planner budgets, or grow runner +chokepoints. Limit edits to source leaves/wiring, new tests/corpus and this report +directory. + +Verification: replay saved proposals through the real runner/admission without +a live model; test positive formation and owner/order/scope/expectation refusal +controls; run actual Node for matching, missing, mismatched and absent-file +cases, including stdout and failure propagation. Exercise registration/frozen +contract hashes and structural-only final acceptance. Run #478/#479/#480 +regressions, the corpus comparison, formatting, all-target Clippy and full Cargo +tests. Parent owns PR, exact-HEAD CI/UAT and integrated release version/hash; +those stages will remain explicitly pending in the worker report. + +Parent design feedback `issue484-design-feedback-01.md` incorporated: + +- Literal grammar is nonempty ASCII alphanumeric plus space and `_./:=@+-`. + Quotes, backslash, newline, backtick, dollar expansion, shell operators and + Unicode are rejected before formation; JSON encoding and outer shell quoting + remain separate operations. Test both rejected characters and real Node + roundtrips for every permitted punctuation. +- Resolve each script key as a set of original explicit literals: equal model + and host values agree; differing values conflict; another key is a separate + preserved duty. The exact known host profile guidance constrains the saved + value's port and never selects among the permitted command spellings. +- Real Node controls fix stdout to the original observed string plus newline + on match, and empty stdout/nonzero exit on comparison failure. Missing script, + missing file, invalid JSON, catch/wrapper and output suppression are controls. + +Keep the existing post-sanitization `original_obligation_sources` for #478 scope +projection. Separately preserve `package_script_formation.sources.plans` at the +earlier profile augmentation boundary for literal provenance; records use JSON +pointers into this immutable snapshot. This avoids reinterpreting legitimate +existing sanitizer transformations as model/host duty loss. + +Execution-path findings and final bounded wiring: + +- The shared verify policy's substring check interpreted the literal `next dev` + as a server launch. Only the whole recognized comparison grammar bypasses that + substring classification; arbitrary Node, shell wrappers and real server + commands retain their existing rejection. Raw retry proposals are checked + before a shell fallback can be normalized into the accepted command. +- The saved 547-character model duty plus 2,237-character host duty cannot fit + the existing 2,500-character step limit. The proposed plan keeps both complete + in two ordered Implement/pass writers of `package.json`, and moves the entire + original check group after both. A dedicated scope leaf requires the original + model ID/text, exact host text on a new ID, exact package scope, no third writer + (including aliases), and host source verify empty. Separate checked views + retain the existing #478 preservation checks and original output boundaries. +- Lint recognizes that same concrete ordered package split, including known + full profile guidance, target-dependent fixed-value comparison and all writer + checks after both writers. Other duplicate ownership remains rejected. + Admission additionally proves both duties against their saved sources. +- Preset/runtime setup optimizations preserve explicit package declarations and + observations instead of retyping their owners or replacing checks with the + profile subset. Both owners must actually execute. No instruction, attempt, + Recovery, test or evidence budget is raised. +- Leading/trailing or repeated spaces are excluded from literals because the + existing command normalizer collapses whitespace. Every accepted spelling + therefore remains unchanged through registration and execution. + +The parent `owner-split-review-01/worker-feedback.md` is covered by +`owner-refusals.json` and `issue484_owner_tests.rs`: model/host text, result and +scope changes; missing/third writers; alias and non-normalized paths; ID reuse; +checks before or between writers; partial groups; nonempty host source verify. + +Final code-review feedback (`code-review-484-01/worker-feedback.md`): the caller +captures each proposal at `strengthen`, then calls `check` before lint. A Ready +result may still be followed by a lint retry, so pending sources must be replaced +for the next proposal until the first `retain`. The capture test exercises that +path and proves the retained original and snapshot belong to the same second +proposal; later `strengthen` cannot change it. An explicit caller-state invariant +refuses two `strengthen` calls without an intervening `check`, including fallback. +This does not pin stale sources from an earlier unretained proposal. + +The original template prompt regression also remains intact: a generic +Implement instruction that merely confirms a port is not promoted to a new +implementation duty by its weak display command alone. The new optimization +boundary applies to explicit fixed declarations, formed comparisons, and Verify +observations. diff --git a/dev-reports/issue-484/formation-evidence.json b/dev-reports/issue-484/formation-evidence.json new file mode 100644 index 00000000..6c73a2dd --- /dev/null +++ b/dev-reports/issue-484/formation-evidence.json @@ -0,0 +1,456 @@ +{ + "execution": "offline saved replay; no live model or application build", + "initial_rejection": { + "event": "recovery_verifier_plan_admission", + "stage": "preclosure_formation", + "classification": "proposal_repairable", + "planner_attempt": 1, + "remaining_planner_attempts": 2, + "recovery_budget_changed": false, + "status": "retry", + "reason": "Recovery verifier binding ProposalRepairable: preclosure weak inline verification requires formation: node -p \"require('./package.json').scripts.dev\": node_smoke_without_assertion (immutable_inline_evidence; app edits cannot strengthen this fixed verifier)\nPackage-script value displays require a fixed literal in saved original model/host obligations; a value absent from the command alone is not grounds for refusal. Use only the supplied saved-literal replacement, including strict comparison and original stdout. Dynamic properties, wrappers, ambiguous/conflicting or absent original literals are unsupported. Never infer expectations from artifacts or a later proposal.\nPreserve each original import target and pass result. Replace a pure import only with a target-specific check in this closed shape: node -e \"import('./original-path.js').then(actual=>{require('node:assert/strict').deepStrictEqual(actual.exportName,42)})\". Supply the actual required export/value, or actual.exportName(...[literal JSON arguments]) and a literal JSON expected result. Alternatively explicitly propose the sorted runtime export-name set using deepStrictEqual(Object.keys(actual).sort(),[literal JSON names]); an explicitly empty set can describe a type-only module. That alternative verifies loadability and runtime export boundary only, not interface fields or business behavior. Do not infer its expectation from a filename. Preserve all original instructions, outputs and owner order. No unrelated assertion, added check beside the weak original, catch or success override discharges it. Unsupported checks must be reproposed with preserved scope or stop within the current budget.\nSaved package-script obligation node -p \"require('./package.json').scripts.dev\" (owner configure-port-and-scripts, expected \"next dev -p 60302\"): replace only with node -e \"const actual=require('./package.json').scripts.dev;require('node:assert/strict').strictEqual(actual,\\\"next dev -p 60302\\\");console.log(actual)\"\nIf complete model and host package instructions exceed one step, explicitly split into two ordered Implement/pass package.json writers: unchanged original model owner first, full original host instruction on a new unique owner second. Keep every original package check together in a Verify/pass after both writers. Do not truncate either instruction, add a third writer or omit host checks.", + "original_scope": { + "goal": "Implement the core task-specific behavior for: 小さな代理店向けのプロジェクト管理アプリを構築してください。", + "steps": [ + { + "expected_paths": [ + "package.json" + ], + "expected_result": "pass", + "id": "inspect-workspace", + "instruction": "Verify the profile-owned package_manifest contract by running every declared check and report any exact failure.", + "kind": "verify", + "verify": [ + "node -p \"['dev'].every(function(k){return String(Object(require('./package.json').scripts)[k]).split(' ').some(function(t,i,a){return t=='next' ? a.slice(i+1).find(function(x){return x})==k : false}) ? String(Object(require('./package.json').scripts)[k]).split(' ').some(function(t,i,a){return t=='--port=60302' ? true : t=='-p' ? a.slice(i+1).find(function(x){return x})=='60302' : t=='-p60302' ? true : t=='--port' ? a.slice(i+1).find(function(x){return x})=='60302' : false}) : false}) ? true : process.exit(1)\"", + "node -p \"['start'].every(function(k){return Object(require('./package.json').scripts)[k] ? String(Object(require('./package.json').scripts)[k]).split(' ').some(function(t,i,a){return t=='next' ? a.slice(i+1).find(function(x){return x})==k : false}) ? String(Object(require('./package.json').scripts)[k]).split(' ').some(function(t,i,a){return t=='--port=60302' ? true : t=='-p' ? a.slice(i+1).find(function(x){return x})=='60302' : t=='-p60302' ? true : t=='--port' ? a.slice(i+1).find(function(x){return x})=='60302' : false}) : false : true}) ? true : process.exit(1)\"", + "node -p \"String(require('./package.json').scripts.build)=='next build' ? true : process.exit(1)\"" + ] + }, + { + "expected_paths": [ + "src/lib/types.ts", + "src/lib/store.ts" + ], + "expected_result": "pass", + "id": "create-types-and-store", + "instruction": "Create src/lib/types.ts exporting domain types: Project (id, name, createdAt), Task (id, projectId, title, deadline, status: 'not-started'|'in-progress'|'completed', assignedMemberId, createdAt), Member (id, name), StatusFilter ('all'|'not-started'|'in-progress'|'completed'), and request/response types (CreateProjectRequest, CreateTaskRequest, CreateMemberRequest, ProjectResponse, TaskResponse, MemberResponse, ErrorResponse with error and optional details). Create src/lib/store.ts exporting functions loadProjects, saveProjects, loadTasks, saveTasks, loadMembers, saveMembers that read/write JSON files under src/data/ (projects.json, tasks.json, members.json). Each load function initialises only on ENOENT (missing file) with an empty array; parse errors or I/O failures throw. Each save function serialises the full array to a temp file then atomically renames it. Use a module-level mutex or generation counter to prevent concurrent lost updates.", + "kind": "implement", + "verify": [ + "test -f src/lib/types.ts", + "test -f src/lib/store.ts" + ] + }, + { + "expected_paths": [ + "src/app/api/projects/route.ts", + "src/app/api/tasks/route.ts", + "src/app/api/members/route.ts" + ], + "expected_result": "pass", + "id": "create-api-routes", + "instruction": "Create three API route modules importing types from src/lib/types.ts and store functions from src/lib/store.ts. 1) src/app/api/projects/route.ts: GET returns {items: Project[]}; POST validates name is non-empty string, creates project with crypto.randomUUID, persists via saveProjects, returns {item: Project} with 201; invalid input returns 400 {error, details}. 2) src/app/api/tasks/route.ts: GET accepts query params projectId and status (StatusFilter), filters tasks accordingly, returns {items: Task[]}; POST validates title, deadline (valid ISO date string), projectId (must exist), assignedMemberId (must exist or empty), status (default 'not-started'), creates task, persists, returns {item: Task} with 201. 3) src/app/api/members/route.ts: GET returns {items: Member[]}; POST validates name, creates member, persists, returns {item: Member} with 201. All error responses use {error, details?} shape. Every function consumed across modules must be exported.", + "kind": "implement", + "verify": [ + "test -f src/app/api/projects/route.ts", + "test -f src/app/api/tasks/route.ts", + "test -f src/app/api/members/route.ts" + ] + }, + { + "expected_paths": [ + "src/app/page.tsx", + "src/app/layout.tsx" + ], + "expected_result": "pass", + "id": "create-page-and-layout", + "instruction": "Rewrite src/app/page.tsx as a client component (\"use client\") that is the single route-bound implementation. It must: (a) import domain types from src/lib/types.ts; (b) render a project creation form with a text input (data-anvil-action=\"input\" on the main name field) and a submit button (data-anvil-action=\"primary\"); (c) render a task creation form with fields for title, deadline (date input), project select, member select, and status; (d) render a status filter dropdown (all, not-started, in-progress, completed) that re-fetches tasks from /api/tasks?status=…; (e) display tasks grouped or listed showing title, deadline, assigned member, and status badge; (f) call fetch to /api/projects, /api/tasks, /api/members on mount and after each mutation; (g) check Response.ok before accepting a mutation response; on !ok parse {error, details?} and display the error message in the UI; (h) set data-anvil-state on the root div to a JSON string snapshot including currentProjects, currentTasks, currentMembers, currentFilter, and any errorMessage after each render; (i) connect create-project, create-task, and filter actions through their API endpoints to persistence and reload committed results. Rewrite src/app/layout.tsx to keep the existing Tailwind metadata and globals.css import, update the html lang to 'ja', and set the title to 'プロジェクト管理'. Ensure no capability component is left unimported; all interactive logic lives in page.tsx.", + "kind": "implement", + "verify": [ + "test -f src/app/page.tsx", + "test -f src/app/layout.tsx" + ] + }, + { + "expected_paths": [ + "package.json" + ], + "expected_result": "pass", + "id": "configure-port-and-scripts", + "instruction": "Update package.json scripts so that dev is 'next dev -p 60302' and start is 'next start -p 60302'. Keep scripts.build as 'next build'. Ensure next, react, react-dom dependencies are present and tailwindcss, postcss, autoprefixer are present. Do not modify tsconfig.json unless it has moduleResolution=node10 or target=ES5—in that case set moduleResolution to 'bundler' and target to 'ES2017'. Keep postcss.config.js plugins including both tailwindcss and autoprefixer. Keep tailwind.config.ts coherent with content paths covering src/app/**/*.tsx.\n\nProfile contract:\nFor the nextjs profile, create a runnable Next.js app, not only package metadata. Keep the project in the workspace root unless a project subdirectory already exists. Preserve the language and styling toolchains declared by an existing project. A JavaScript project using plain CSS requires package.json, an App Router page and layout, and the imported stylesheet; do not add TypeScript, @types, Tailwind, PostCSS, or Autoprefixer only to satisfy a template. A TypeScript project additionally requires a coherent tsconfig and type dependencies. If those mode-appropriate files are absent, write the coherent App Router scaffold before further inspection. package.json must include compatible next, react, and react-dom dependencies plus scripts.build = `next build`. If Tailwind is used, package.json must include tailwindcss/postcss/autoprefixer and postcss.config plugins must include BOTH tailwindcss and autoprefixer. For TypeScript/TSX apps, create tsconfig.json before treating the app as complete. Keep a single route-bound implementation; do not leave capability components unimported. Do not use deprecated moduleResolution=node10 or target=ES5; prefer moduleResolution=bundler and target=ES2017 or newer. For interactive UI, expose task-agnostic observability hooks: data-anvil-action=\"primary\" on the main start/submit/action control, data-anvil-action=\"input\" on the main text entry surface when one exists, and data-anvil-state containing JSON for meaningful visible state after each render. The data-anvil-state snapshot must include at least one dimension that immediately responds to input, such as the current selection or edited value. When the contract includes start_or_restart_flow, every restart affordance (including terminal and active states when present) should carry data-anvil-action=\"restart\"; the initial primary action alone cannot satisfy recovery verification.", + "kind": "implement", + "verify": [ + "node -p \"require('./package.json').scripts.dev\"", + "node -p \"['dev'].every(function(k){return String(Object(require('./package.json').scripts)[k]).split(' ').some(function(t,i,a){return t=='next' ? a.slice(i+1).find(function(x){return x})==k : false}) ? String(Object(require('./package.json').scripts)[k]).split(' ').some(function(t,i,a){return t=='--port=60302' ? true : t=='-p' ? a.slice(i+1).find(function(x){return x})=='60302' : t=='-p60302' ? true : t=='--port' ? a.slice(i+1).find(function(x){return x})=='60302' : false}) : false}) ? true : process.exit(1)\"", + "node -p \"['start'].every(function(k){return Object(require('./package.json').scripts)[k] ? String(Object(require('./package.json').scripts)[k]).split(' ').some(function(t,i,a){return t=='next' ? a.slice(i+1).find(function(x){return x})==k : false}) ? String(Object(require('./package.json').scripts)[k]).split(' ').some(function(t,i,a){return t=='--port=60302' ? true : t=='-p' ? a.slice(i+1).find(function(x){return x})=='60302' : t=='-p60302' ? true : t=='--port' ? a.slice(i+1).find(function(x){return x})=='60302' : false}) : false : true}) ? true : process.exit(1)\"", + "node -p \"String(require('./package.json').scripts.build)=='next build' ? true : process.exit(1)\"" + ] + }, + { + "expected_paths": [], + "expected_result": "pass", + "id": "build-verification", + "instruction": "Run npm run build to verify the full Next.js TypeScript application compiles, all API routes type-check, the client component in src/app/page.tsx imports types from src/lib/types.ts and helpers from src/lib/store.ts without errors, and no unimported capability modules remain. The build must succeed with exit code 0.", + "kind": "verify", + "verify": [ + "npm run build" + ] + } + ] + } + }, + "planner_requests": 2, + "source_manifest": { + "capture": "preclosure attempt 1; live-prefix observation, not terminal evidence", + "derived_fixture": "formed-plan.json replaces the weak command and explicitly splits the complete saved model and host package duties to stay within the unchanged instruction limit; all package checks follow both owners.", + "expected_literal": "next dev -p 60302", + "fixture_sha256": { + "formed-plan.json": "279f565fd9a7a0c8962b2907e02e1fdf7d47d2fbd38bd772e0c12d6fdd27320d", + "original-plan.json": "fbdaa12997a87c4eb78ca2029d7d5eda3f5d2acf7e9cc6df010930db96302dee", + "package.json": "ad2c5affb307d8da85b8fad196d83e89ae3fd4e76133d5b41b903e89613b087c", + "saved-obligation-sources.json": "278c86e3c8a172b493236bb06ae71610eadf9d147b180ed85aebdb9dab8f076e", + "saved-scope.json": "cf1f328b394b1f9df6b11372180fab81cccf3fd21292c6acf19e847a86f56af8" + }, + "json_pointer": "/events/4/event/original_obligation_sources/model/steps/4", + "owner": "configure-port-and-scripts", + "source": "workspace/management/runs/20260914-integrated-r0-evaluation-478-480-01/reports/preclosure-admission-observations-01.json", + "source_sha256": "248d0ab5a8958e623aa11d3f857fedc45ed6576c545dc4387b2f779da383bd29" + }, + "validated": { + "event": "preclosure_verifier_replacements_validated", + "scope_preserved": true, + "replacements": [ + { + "expected_result": "pass", + "original_command": "node -p \"require('./package.json').scripts.dev\"", + "package_script": { + "expected_literal": "next dev -p 60302", + "expected_result": "pass", + "failure_propagation": "uncaught require/property/assertion failure exits nonzero", + "original_check_owner": "configure-port-and-scripts", + "original_command": "node -p \"require('./package.json').scripts.dev\"", + "original_command_sha256": "1d1192f79327ad4a92d503ecad1d8db9ba863393d1c25509915cf56d5bcbeea3", + "original_step_index": 4, + "output": "print actual string plus newline after strict comparison", + "replacement_command": "node -e \"const actual=require('./package.json').scripts.dev;require('node:assert/strict').strictEqual(actual,\\\"next dev -p 60302\\\");console.log(actual)\"", + "replacement_command_sha256": "f5cc778148d3cf44a2f44c65aa9a4a647a81af0f14215d627a1ace818d140f3e", + "script": "dev", + "sources": [ + { + "fixed_literals": [ + "next dev -p 60302" + ], + "instruction_sha256": "dc56920ec1bfdff9abfccf2a0d38b6f490f23fd8077cbc293aa29b3da32b3838", + "json_pointer": "/package_script_formation/sources/plans/model/steps/4", + "kind": "model", + "owner": "configure-port-and-scripts", + "step_index": 4, + "step_sha256": "f9967ef60c8e127a77422a53e690444bc679345d70df62953c44fc318016f5c9" + }, + { + "fixed_literals": [], + "instruction_sha256": "b2dd0b0c696f9c4b38489821dd2917273a8e139d0b43c8cd6bc7d66e0d0975d2", + "json_pointer": "/package_script_formation/sources/plans/host/steps/0", + "kind": "host", + "owner": "configure-port-and-scripts", + "step_index": 0, + "step_sha256": "e37b7a17aa4eff62f940c289f8f38404f4018eeabaf77a600b3a57596532312a" + } + ], + "target": "./package.json" + }, + "reason": "saved_package_script_literal_strict_comparison_then_original_output", + "replacement_command": "node -e \"const actual=require('./package.json').scripts.dev;require('node:assert/strict').strictEqual(actual,\\\"next dev -p 60302\\\");console.log(actual)\"" + } + ], + "formed_plan": { + "goal": "Implement the core task-specific behavior for: 小さな代理店向けのプロジェクト管理アプリを構築してください。", + "steps": [ + { + "expected_paths": [ + "package.json" + ], + "expected_result": "pass", + "id": "inspect-workspace", + "instruction": "Verify the profile-owned package_manifest contract by running every declared check and report any exact failure.", + "kind": "verify", + "verify": [ + "node -p \"['dev'].every(function(k){return String(Object(require('./package.json').scripts)[k]).split(' ').some(function(t,i,a){return t=='next' ? a.slice(i+1).find(function(x){return x})==k : false}) ? String(Object(require('./package.json').scripts)[k]).split(' ').some(function(t,i,a){return t=='--port=60302' ? true : t=='-p' ? a.slice(i+1).find(function(x){return x})=='60302' : t=='-p60302' ? true : t=='--port' ? a.slice(i+1).find(function(x){return x})=='60302' : false}) : false}) ? true : process.exit(1)\"", + "node -p \"['start'].every(function(k){return Object(require('./package.json').scripts)[k] ? String(Object(require('./package.json').scripts)[k]).split(' ').some(function(t,i,a){return t=='next' ? a.slice(i+1).find(function(x){return x})==k : false}) ? String(Object(require('./package.json').scripts)[k]).split(' ').some(function(t,i,a){return t=='--port=60302' ? true : t=='-p' ? a.slice(i+1).find(function(x){return x})=='60302' : t=='-p60302' ? true : t=='--port' ? a.slice(i+1).find(function(x){return x})=='60302' : false}) : false : true}) ? true : process.exit(1)\"", + "node -p \"String(require('./package.json').scripts.build)=='next build' ? true : process.exit(1)\"" + ] + }, + { + "expected_paths": [ + "src/lib/types.ts", + "src/lib/store.ts" + ], + "expected_result": "pass", + "id": "create-types-and-store", + "instruction": "Create src/lib/types.ts exporting domain types: Project (id, name, createdAt), Task (id, projectId, title, deadline, status: 'not-started'|'in-progress'|'completed', assignedMemberId, createdAt), Member (id, name), StatusFilter ('all'|'not-started'|'in-progress'|'completed'), and request/response types (CreateProjectRequest, CreateTaskRequest, CreateMemberRequest, ProjectResponse, TaskResponse, MemberResponse, ErrorResponse with error and optional details). Create src/lib/store.ts exporting functions loadProjects, saveProjects, loadTasks, saveTasks, loadMembers, saveMembers that read/write JSON files under src/data/ (projects.json, tasks.json, members.json). Each load function initialises only on ENOENT (missing file) with an empty array; parse errors or I/O failures throw. Each save function serialises the full array to a temp file then atomically renames it. Use a module-level mutex or generation counter to prevent concurrent lost updates.", + "kind": "implement", + "verify": [ + "test -f src/lib/types.ts", + "test -f src/lib/store.ts" + ] + }, + { + "expected_paths": [ + "src/app/api/projects/route.ts", + "src/app/api/tasks/route.ts", + "src/app/api/members/route.ts" + ], + "expected_result": "pass", + "id": "create-api-routes", + "instruction": "Create three API route modules importing types from src/lib/types.ts and store functions from src/lib/store.ts. 1) src/app/api/projects/route.ts: GET returns {items: Project[]}; POST validates name is non-empty string, creates project with crypto.randomUUID, persists via saveProjects, returns {item: Project} with 201; invalid input returns 400 {error, details}. 2) src/app/api/tasks/route.ts: GET accepts query params projectId and status (StatusFilter), filters tasks accordingly, returns {items: Task[]}; POST validates title, deadline (valid ISO date string), projectId (must exist), assignedMemberId (must exist or empty), status (default 'not-started'), creates task, persists, returns {item: Task} with 201. 3) src/app/api/members/route.ts: GET returns {items: Member[]}; POST validates name, creates member, persists, returns {item: Member} with 201. All error responses use {error, details?} shape. Every function consumed across modules must be exported.", + "kind": "implement", + "verify": [ + "test -f src/app/api/projects/route.ts", + "test -f src/app/api/tasks/route.ts", + "test -f src/app/api/members/route.ts" + ] + }, + { + "expected_paths": [ + "src/app/page.tsx", + "src/app/layout.tsx" + ], + "expected_result": "pass", + "id": "create-page-and-layout", + "instruction": "Rewrite src/app/page.tsx as a client component (\"use client\") that is the single route-bound implementation. It must: (a) import domain types from src/lib/types.ts; (b) render a project creation form with a text input (data-anvil-action=\"input\" on the main name field) and a submit button (data-anvil-action=\"primary\"); (c) render a task creation form with fields for title, deadline (date input), project select, member select, and status; (d) render a status filter dropdown (all, not-started, in-progress, completed) that re-fetches tasks from /api/tasks?status=…; (e) display tasks grouped or listed showing title, deadline, assigned member, and status badge; (f) call fetch to /api/projects, /api/tasks, /api/members on mount and after each mutation; (g) check Response.ok before accepting a mutation response; on !ok parse {error, details?} and display the error message in the UI; (h) set data-anvil-state on the root div to a JSON string snapshot including currentProjects, currentTasks, currentMembers, currentFilter, and any errorMessage after each render; (i) connect create-project, create-task, and filter actions through their API endpoints to persistence and reload committed results. Rewrite src/app/layout.tsx to keep the existing Tailwind metadata and globals.css import, update the html lang to 'ja', and set the title to 'プロジェクト管理'. Ensure no capability component is left unimported; all interactive logic lives in page.tsx.", + "kind": "implement", + "verify": [ + "test -f src/app/page.tsx", + "test -f src/app/layout.tsx" + ] + }, + { + "expected_paths": [ + "package.json" + ], + "expected_result": "pass", + "id": "configure-port-and-scripts", + "instruction": "Update package.json scripts so that dev is 'next dev -p 60302' and start is 'next start -p 60302'. Keep scripts.build as 'next build'. Ensure next, react, react-dom dependencies are present and tailwindcss, postcss, autoprefixer are present. Do not modify tsconfig.json unless it has moduleResolution=node10 or target=ES5—in that case set moduleResolution to 'bundler' and target to 'ES2017'. Keep postcss.config.js plugins including both tailwindcss and autoprefixer. Keep tailwind.config.ts coherent with content paths covering src/app/**/*.tsx.", + "kind": "implement", + "verify": [ + "node -p \"['dev'].every(function(k){return String(Object(require('./package.json').scripts)[k]).split(' ').some(function(t,i,a){return t=='next' ? a.slice(i+1).find(function(x){return x})==k : false}) ? String(Object(require('./package.json').scripts)[k]).split(' ').some(function(t,i,a){return t=='--port=60302' ? true : t=='-p' ? a.slice(i+1).find(function(x){return x})=='60302' : t=='-p60302' ? true : t=='--port' ? a.slice(i+1).find(function(x){return x})=='60302' : false}) : false}) ? true : process.exit(1)\"", + "node -p \"['start'].every(function(k){return Object(require('./package.json').scripts)[k] ? String(Object(require('./package.json').scripts)[k]).split(' ').some(function(t,i,a){return t=='next' ? a.slice(i+1).find(function(x){return x})==k : false}) ? String(Object(require('./package.json').scripts)[k]).split(' ').some(function(t,i,a){return t=='--port=60302' ? true : t=='-p' ? a.slice(i+1).find(function(x){return x})=='60302' : t=='-p60302' ? true : t=='--port' ? a.slice(i+1).find(function(x){return x})=='60302' : false}) : false : true}) ? true : process.exit(1)\"", + "node -p \"String(require('./package.json').scripts.build)=='next build' ? true : process.exit(1)\"" + ] + }, + { + "expected_paths": [ + "package.json" + ], + "expected_result": "pass", + "id": "preserve-profile-package-duty", + "instruction": "For the nextjs profile, create a runnable Next.js app, not only package metadata. Keep the project in the workspace root unless a project subdirectory already exists. Preserve the language and styling toolchains declared by an existing project. A JavaScript project using plain CSS requires package.json, an App Router page and layout, and the imported stylesheet; do not add TypeScript, @types, Tailwind, PostCSS, or Autoprefixer only to satisfy a template. A TypeScript project additionally requires a coherent tsconfig and type dependencies. If those mode-appropriate files are absent, write the coherent App Router scaffold before further inspection. package.json must include compatible next, react, and react-dom dependencies plus scripts.build = `next build`. If Tailwind is used, package.json must include tailwindcss/postcss/autoprefixer and postcss.config plugins must include BOTH tailwindcss and autoprefixer. For TypeScript/TSX apps, create tsconfig.json before treating the app as complete. Keep a single route-bound implementation; do not leave capability components unimported. Do not use deprecated moduleResolution=node10 or target=ES5; prefer moduleResolution=bundler and target=ES2017 or newer. For interactive UI, expose task-agnostic observability hooks: data-anvil-action=\"primary\" on the main start/submit/action control, data-anvil-action=\"input\" on the main text entry surface when one exists, and data-anvil-state containing JSON for meaningful visible state after each render. The data-anvil-state snapshot must include at least one dimension that immediately responds to input, such as the current selection or edited value. When the contract includes start_or_restart_flow, every restart affordance (including terminal and active states when present) should carry data-anvil-action=\"restart\"; the initial primary action alone cannot satisfy recovery verification. A restart reachable during active use (hook or keyboard shortcut) allows behavioral verification, while an overlay-only restart may verify as unverified:terminal_state_not_reached (partial). The dev/start scripts must run on the explicitly requested port 60302: `next dev -p 60302` and `next start -p 60302` or equivalent `--port 60302` forms.", + "kind": "implement", + "verify": [ + "node -p \"['dev'].every(function(k){return String(Object(require('./package.json').scripts)[k]).split(' ').some(function(t,i,a){return t=='next' ? a.slice(i+1).find(function(x){return x})==k : false}) ? String(Object(require('./package.json').scripts)[k]).split(' ').some(function(t,i,a){return t=='--port=60302' ? true : t=='-p' ? a.slice(i+1).find(function(x){return x})=='60302' : t=='-p60302' ? true : t=='--port' ? a.slice(i+1).find(function(x){return x})=='60302' : false}) : false}) ? true : process.exit(1)\"", + "node -p \"['start'].every(function(k){return Object(require('./package.json').scripts)[k] ? String(Object(require('./package.json').scripts)[k]).split(' ').some(function(t,i,a){return t=='next' ? a.slice(i+1).find(function(x){return x})==k : false}) ? String(Object(require('./package.json').scripts)[k]).split(' ').some(function(t,i,a){return t=='--port=60302' ? true : t=='-p' ? a.slice(i+1).find(function(x){return x})=='60302' : t=='-p60302' ? true : t=='--port' ? a.slice(i+1).find(function(x){return x})=='60302' : false}) : false : true}) ? true : process.exit(1)\"", + "node -p \"String(require('./package.json').scripts.build)=='next build' ? true : process.exit(1)\"" + ] + }, + { + "expected_paths": [], + "expected_result": "pass", + "id": "verify-package-scripts", + "instruction": "Run every original package check after both package owners and preserve stdout and nonzero failures.", + "kind": "verify", + "verify": [ + "node -e \"const actual=require('./package.json').scripts.dev;require('node:assert/strict').strictEqual(actual,\\\"next dev -p 60302\\\");console.log(actual)\"", + "node -p \"['dev'].every(function(k){return String(Object(require('./package.json').scripts)[k]).split(' ').some(function(t,i,a){return t=='next' ? a.slice(i+1).find(function(x){return x})==k : false}) ? String(Object(require('./package.json').scripts)[k]).split(' ').some(function(t,i,a){return t=='--port=60302' ? true : t=='-p' ? a.slice(i+1).find(function(x){return x})=='60302' : t=='-p60302' ? true : t=='--port' ? a.slice(i+1).find(function(x){return x})=='60302' : false}) : false}) ? true : process.exit(1)\"", + "node -p \"['start'].every(function(k){return Object(require('./package.json').scripts)[k] ? String(Object(require('./package.json').scripts)[k]).split(' ').some(function(t,i,a){return t=='next' ? a.slice(i+1).find(function(x){return x})==k : false}) ? String(Object(require('./package.json').scripts)[k]).split(' ').some(function(t,i,a){return t=='--port=60302' ? true : t=='-p' ? a.slice(i+1).find(function(x){return x})=='60302' : t=='-p60302' ? true : t=='--port' ? a.slice(i+1).find(function(x){return x})=='60302' : false}) : false : true}) ? true : process.exit(1)\"", + "node -p \"String(require('./package.json').scripts.build)=='next build' ? true : process.exit(1)\"" + ] + }, + { + "expected_paths": [], + "expected_result": "pass", + "id": "build-verification", + "instruction": "Run npm run build to verify the full Next.js TypeScript application compiles, all API routes type-check, the client component in src/app/page.tsx imports types from src/lib/types.ts and helpers from src/lib/store.ts without errors, and no unimported capability modules remain. The build must succeed with exit code 0.", + "kind": "verify", + "verify": [ + "npm run build" + ] + } + ] + }, + "package_script_formation": { + "obligations": [ + { + "expected_literal": "next dev -p 60302", + "expected_result": "pass", + "failure_propagation": "uncaught require/property/assertion failure exits nonzero", + "original_check_owner": "configure-port-and-scripts", + "original_command": "node -p \"require('./package.json').scripts.dev\"", + "original_command_sha256": "1d1192f79327ad4a92d503ecad1d8db9ba863393d1c25509915cf56d5bcbeea3", + "original_step_index": 4, + "output": "print actual string plus newline after strict comparison", + "replacement_command": "node -e \"const actual=require('./package.json').scripts.dev;require('node:assert/strict').strictEqual(actual,\\\"next dev -p 60302\\\");console.log(actual)\"", + "replacement_command_sha256": "f5cc778148d3cf44a2f44c65aa9a4a647a81af0f14215d627a1ace818d140f3e", + "script": "dev", + "sources": [ + { + "fixed_literals": [ + "next dev -p 60302" + ], + "instruction_sha256": "dc56920ec1bfdff9abfccf2a0d38b6f490f23fd8077cbc293aa29b3da32b3838", + "json_pointer": "/package_script_formation/sources/plans/model/steps/4", + "kind": "model", + "owner": "configure-port-and-scripts", + "step_index": 4, + "step_sha256": "f9967ef60c8e127a77422a53e690444bc679345d70df62953c44fc318016f5c9" + }, + { + "fixed_literals": [], + "instruction_sha256": "b2dd0b0c696f9c4b38489821dd2917273a8e139d0b43c8cd6bc7d66e0d0975d2", + "json_pointer": "/package_script_formation/sources/plans/host/steps/0", + "kind": "host", + "owner": "configure-port-and-scripts", + "step_index": 0, + "step_sha256": "e37b7a17aa4eff62f940c289f8f38404f4018eeabaf77a600b3a57596532312a" + } + ], + "target": "./package.json" + } + ], + "original_verify_commands": [ + "node -p \"['dev'].every(function(k){return String(Object(require('./package.json').scripts)[k]).split(' ').some(function(t,i,a){return t=='next' ? a.slice(i+1).find(function(x){return x})==k : false}) ? String(Object(require('./package.json').scripts)[k]).split(' ').some(function(t,i,a){return t=='--port=60302' ? true : t=='-p' ? a.slice(i+1).find(function(x){return x})=='60302' : t=='-p60302' ? true : t=='--port' ? a.slice(i+1).find(function(x){return x})=='60302' : false}) : false}) ? true : process.exit(1)\"", + "node -p \"['start'].every(function(k){return Object(require('./package.json').scripts)[k] ? String(Object(require('./package.json').scripts)[k]).split(' ').some(function(t,i,a){return t=='next' ? a.slice(i+1).find(function(x){return x})==k : false}) ? String(Object(require('./package.json').scripts)[k]).split(' ').some(function(t,i,a){return t=='--port=60302' ? true : t=='-p' ? a.slice(i+1).find(function(x){return x})=='60302' : t=='-p60302' ? true : t=='--port' ? a.slice(i+1).find(function(x){return x})=='60302' : false}) : false : true}) ? true : process.exit(1)\"", + "node -p \"String(require('./package.json').scripts.build)=='next build' ? true : process.exit(1)\"", + "test -f src/lib/types.ts", + "test -f src/lib/store.ts", + "test -f src/app/api/projects/route.ts", + "test -f src/app/api/tasks/route.ts", + "test -f src/app/api/members/route.ts", + "test -f src/app/page.tsx", + "test -f src/app/layout.tsx", + "node -p \"require('./package.json').scripts.dev\"", + "node -p \"['dev'].every(function(k){return String(Object(require('./package.json').scripts)[k]).split(' ').some(function(t,i,a){return t=='next' ? a.slice(i+1).find(function(x){return x})==k : false}) ? String(Object(require('./package.json').scripts)[k]).split(' ').some(function(t,i,a){return t=='--port=60302' ? true : t=='-p' ? a.slice(i+1).find(function(x){return x})=='60302' : t=='-p60302' ? true : t=='--port' ? a.slice(i+1).find(function(x){return x})=='60302' : false}) : false}) ? true : process.exit(1)\"", + "node -p \"['start'].every(function(k){return Object(require('./package.json').scripts)[k] ? String(Object(require('./package.json').scripts)[k]).split(' ').some(function(t,i,a){return t=='next' ? a.slice(i+1).find(function(x){return x})==k : false}) ? String(Object(require('./package.json').scripts)[k]).split(' ').some(function(t,i,a){return t=='--port=60302' ? true : t=='-p' ? a.slice(i+1).find(function(x){return x})=='60302' : t=='-p60302' ? true : t=='--port' ? a.slice(i+1).find(function(x){return x})=='60302' : false}) : false : true}) ? true : process.exit(1)\"", + "node -p \"String(require('./package.json').scripts.build)=='next build' ? true : process.exit(1)\"", + "npm run build" + ], + "refusals": [], + "registered_contract": { + "deferred_verify_requirements": [], + "deterministic_oracles": [], + "evidence_hint_tokens": [], + "goal": null, + "profile": null, + "protected_paths": [], + "required_capabilities": [], + "required_evidence": [], + "required_obligations": [], + "required_paths": [], + "verify_commands": [], + "verify_repair_cap": 2 + }, + "registered_contract_sha256": "27535dc740b6f1b95a6bf254fc2f3cfea67d896079d7f3e65bb7f7d2c2174aba", + "sources": { + "acquisition_stage": "profile_augmentation_before_sanitization", + "host_sha256": "5530508b5950c91ca79d8f9597777a8e1ee4225eb732784426780cb950bc0c7c", + "model_sha256": "6b9d5fe4357ecdbd5ccc79932b43a8cdc160b6b9d085af2f823bd96f885f7d69", + "plans": { + "host": { + "goal": "Original ultra goal: 小さな代理店向けのプロジェクト管理アプリを構築してください。ユーザーはプロジェクトの作成、期限付きタスクの追加、チームメンバーへの割り当てができ、進捗状況(未着手・進行中・完了)でタスクを絞り込めるようにします。60302ポートで起動可能なNext.jsアプリとして開発してください。\nProfile: nextjs\nStyle: default\nIntent: create\nPhase id: core-implementation\nPhase task: Implement the core task-specific behavior for: 小さな代理店向けのプロジェクト管理アプリを構築してください。ユーザーはプロジェクトの作成、期限付きタスクの追加、チームメンバーへの割り当てができ、進捗状況(未着手・進行中・完了)でタスクを絞り込めるようにします。60302ポートで起動可能なNext.jsアプリとして開発してください。. Keep one route-bound implementation, implement the requested interface and replace unrelated scaffold content, and keep the implementation in the Next.js route-bound source. For TypeScript apps, put shared domain and request/response types in src/lib/types.ts and import the same types from page and route modules. Use imported helpers and export every function consumed by another module. Decide one JSON response shape per operation (bare array, {items}, or {item}) and make the UI read exactly what the route returns. Check Response.ok before accepting a mutation; return errors as {error, details?} and display them in the UI. Connect each requested UI action through its API to persistence and reload the committed result.\n\nWorkspace snapshot:\n- .next\n- next-env.d.ts\n- node_modules\n- package-lock.json\n- package.json\n- postcss.config.js\n- src\n- tailwind.config.ts\n- tsconfig.json\n\nPrior ultra context:\n- Completed phases:\n - project-setup (2/2)\n\nUnmet final requirements from earlier phases:\n- none\n\nRequested features not yet detected:\n- accepting\n- affordance\n- alone\n- another\n- anvil\n- api\n- … and 112 more\n\nProfile generation rules:\n- Profile nextjs/create: preserve a real Next.js app contract. Include next/react/react-dom dependencies, keep scripts.build as next build, and end with a build verification phase. Put dependency setup before any npm run build verification when node_modules is not already present; setup instructions may install dependencies, but verify must not contain npm install. If dependency setup is not allowed or cannot run, stop with dependency_missing instead of claiming build success. Keep a single route-bound implementation; do not leave capability components unimported. For interactive UI, implement the requested interface, replacing unrelated scaffold content, and preserve data-anvil-* attributes on route-bound UI: data-anvil-action=\"primary\" on the main start/submit/action control, data-anvil-action=\"input\" on the main text entry surface when one exists, and data-anvil-state with a JSON snapshot of meaningful visible state after each render. When the contract includes start_or_restart_flow, every restart affordance (including terminal and active states when present) should carry data-anvil-action=\"restart\"; the initial primary action alone cannot satisfy recovery verification. A restart reachable during active use (hook or keyboard shortcut) allows behavioral verification, while an overlay-only restart may verify as unverified:terminal_state_not_reached (partial). If you use Tailwind utility classes or @tailwind directives, include tailwindcss/postcss/autoprefixer and create tailwind.config.* plus postcss.config.*; postcss.config plugins must include BOTH tailwindcss and autoprefixer. Otherwise use plain CSS and do not write Tailwind utility classes. Keep scripts.dev and scripts.start on the explicit requested port when the goal or plan requests one; otherwise use port 3011 with next dev/start -p 3011 or --port 3011.\n- For TypeScript apps, put shared domain and request/response types in src/lib/types.ts and import the same types from page and route modules. Use imported helpers and export every function consumed by another module. Decide one JSON response shape per operation (bare array, {items}, or {item}) and make the UI read exactly what the route returns. Check Response.ok before accepting a mutation; return errors as {error, details?} and display them in the UI. Connect each requested UI action through its API to persistence and reload the committed result.\n- For server-side file persistence, initialize only a missing file (ENOENT); distinguish valid empty data from missing data. Parse errors, directory replacement and other read or write I/O failures must return 5xx without overwriting any existing or related data. Serialize the complete read-modify-write transaction across all writers (including multiple processes), or use generation checks with conflict handling; write to a temporary file and atomically rename it. Atomic rename alone does not prevent lost updates.\n- Validate the complete proposed state before committing: aggregate quantities by entity key before checking availability, scope totals to the requested period, define rejection/cascade/reassignment for referenced parent deletion, and match strict/inclusive comparisons to the goal wording. Reject non-finite or invalid quantities and amounts at the API boundary.\n\nProfile runtime contract:\n- Preserve the workspace as a real Next.js app.\n- Keep next/react/react-dom dependencies in package.json.\n- Keep scripts.build as next build; do not replace it with echo/skip/no-op commands.\n- If npm run build cannot run because dependencies are not installed, report dependency_missing or use an explicit setup step; do not fake success.\n- Keep scripts.dev and scripts.start on the explicitly requested port 60302: next dev/start -p 60302 or --port 60302.\n- If using Tailwind utility classes or @tailwind directives, keep the Tailwind toolchain complete: tailwindcss/postcss/autoprefixer dependencies, tailwind.config.*, and postcss.config plugins with BOTH tailwindcss and autoprefixer. Otherwise use plain CSS.\n- Keep TypeScript and app router configuration coherent.\n- Keep a single route-bound implementation; do not leave capability components unimported.\n- For interactive UI, expose data-anvil-action=\"primary\" on the main start/submit/action control, data-anvil-action=\"input\" on the main text entry surface when one exists, and data-anvil-state with a JSON snapshot of meaningful visible state after each render. When the contract includes start_or_restart_flow, every restart affordance (including terminal and active states when present) should carry data-anvil-action=\"restart\"; the initial primary action alone cannot satisfy recovery verification. A restart reachable during active use (hook or keyboard shortcut) allows behavioral verification, while an overlay-only restart may verify as unverified:terminal_state_not_reached (partial).\n- Do not treat scaffold-only, package-only, or build-only output as complete.\nRoute-bound implementation constraint:\n- Keep a single route-bound implementation; do not leave capability components unimported.\n\nPre-provisioned scaffold note:\n- Required scaffold files are authored before phase 1 when absent; verify or extend the scaffold rather than re-planning file creation.\nDeterministic verification preference:\n- npm run build\n\n\nRequired final artifacts:\n- package.json\n- tsconfig.json\n- postcss.config.js\n- tailwind.config.ts\n- src/app/layout.tsx\n- src/app/page.tsx\n- src/app/globals.css\n- src/app/global.d.ts\n\nRequired final capabilities:\n- stateful_interaction\n- user_input_or_action\n- visible_state_change\n- persistence\n\nRequired final evidence:\n- nextjs_route_evidence\n- build_command_or_dependency_missing_boundary\n- implementation_artifact\n- visible_interactive_surface_evidence\n- user_input_handler_evidence\n- stateful_update_evidence\n- interactive_ui_source_evidence\n- non_static_screen_evidence\n- persistence_evidence", + "steps": [ + { + "expected_paths": [ + "package.json" + ], + "expected_result": "pass", + "id": "configure-port-and-scripts", + "instruction": "For the nextjs profile, create a runnable Next.js app, not only package metadata. Keep the project in the workspace root unless a project subdirectory already exists. Preserve the language and styling toolchains declared by an existing project. A JavaScript project using plain CSS requires package.json, an App Router page and layout, and the imported stylesheet; do not add TypeScript, @types, Tailwind, PostCSS, or Autoprefixer only to satisfy a template. A TypeScript project additionally requires a coherent tsconfig and type dependencies. If those mode-appropriate files are absent, write the coherent App Router scaffold before further inspection. package.json must include compatible next, react, and react-dom dependencies plus scripts.build = `next build`. If Tailwind is used, package.json must include tailwindcss/postcss/autoprefixer and postcss.config plugins must include BOTH tailwindcss and autoprefixer. For TypeScript/TSX apps, create tsconfig.json before treating the app as complete. Keep a single route-bound implementation; do not leave capability components unimported. Do not use deprecated moduleResolution=node10 or target=ES5; prefer moduleResolution=bundler and target=ES2017 or newer. For interactive UI, expose task-agnostic observability hooks: data-anvil-action=\"primary\" on the main start/submit/action control, data-anvil-action=\"input\" on the main text entry surface when one exists, and data-anvil-state containing JSON for meaningful visible state after each render. The data-anvil-state snapshot must include at least one dimension that immediately responds to input, such as the current selection or edited value. When the contract includes start_or_restart_flow, every restart affordance (including terminal and active states when present) should carry data-anvil-action=\"restart\"; the initial primary action alone cannot satisfy recovery verification. A restart reachable during active use (hook or keyboard shortcut) allows behavioral verification, while an overlay-only restart may verify as unverified:terminal_state_not_reached (partial). The dev/start scripts must run on the explicitly requested port 60302: `next dev -p 60302` and `next start -p 60302` or equivalent `--port 60302` forms.", + "kind": "implement", + "verify": [] + } + ] + }, + "model": { + "goal": "Original ultra goal: 小さな代理店向けのプロジェクト管理アプリを構築してください。ユーザーはプロジェクトの作成、期限付きタスクの追加、チームメンバーへの割り当てができ、進捗状況(未着手・進行中・完了)でタスクを絞り込めるようにします。60302ポートで起動可能なNext.jsアプリとして開発してください。\nProfile: nextjs\nStyle: default\nIntent: create\nPhase id: core-implementation\nPhase task: Implement the core task-specific behavior for: 小さな代理店向けのプロジェクト管理アプリを構築してください。ユーザーはプロジェクトの作成、期限付きタスクの追加、チームメンバーへの割り当てができ、進捗状況(未着手・進行中・完了)でタスクを絞り込めるようにします。60302ポートで起動可能なNext.jsアプリとして開発してください。. Keep one route-bound implementation, implement the requested interface and replace unrelated scaffold content, and keep the implementation in the Next.js route-bound source. For TypeScript apps, put shared domain and request/response types in src/lib/types.ts and import the same types from page and route modules. Use imported helpers and export every function consumed by another module. Decide one JSON response shape per operation (bare array, {items}, or {item}) and make the UI read exactly what the route returns. Check Response.ok before accepting a mutation; return errors as {error, details?} and display them in the UI. Connect each requested UI action through its API to persistence and reload the committed result.\n\nWorkspace snapshot:\n- .next\n- next-env.d.ts\n- node_modules\n- package-lock.json\n- package.json\n- postcss.config.js\n- src\n- tailwind.config.ts\n- tsconfig.json\n\nPrior ultra context:\n- Completed phases:\n - project-setup (2/2)\n\nUnmet final requirements from earlier phases:\n- none\n\nRequested features not yet detected:\n- accepting\n- affordance\n- alone\n- another\n- anvil\n- api\n- … and 112 more\n\nProfile generation rules:\n- Profile nextjs/create: preserve a real Next.js app contract. Include next/react/react-dom dependencies, keep scripts.build as next build, and end with a build verification phase. Put dependency setup before any npm run build verification when node_modules is not already present; setup instructions may install dependencies, but verify must not contain npm install. If dependency setup is not allowed or cannot run, stop with dependency_missing instead of claiming build success. Keep a single route-bound implementation; do not leave capability components unimported. For interactive UI, implement the requested interface, replacing unrelated scaffold content, and preserve data-anvil-* attributes on route-bound UI: data-anvil-action=\"primary\" on the main start/submit/action control, data-anvil-action=\"input\" on the main text entry surface when one exists, and data-anvil-state with a JSON snapshot of meaningful visible state after each render. When the contract includes start_or_restart_flow, every restart affordance (including terminal and active states when present) should carry data-anvil-action=\"restart\"; the initial primary action alone cannot satisfy recovery verification. A restart reachable during active use (hook or keyboard shortcut) allows behavioral verification, while an overlay-only restart may verify as unverified:terminal_state_not_reached (partial). If you use Tailwind utility classes or @tailwind directives, include tailwindcss/postcss/autoprefixer and create tailwind.config.* plus postcss.config.*; postcss.config plugins must include BOTH tailwindcss and autoprefixer. Otherwise use plain CSS and do not write Tailwind utility classes. Keep scripts.dev and scripts.start on the explicit requested port when the goal or plan requests one; otherwise use port 3011 with next dev/start -p 3011 or --port 3011.\n- For TypeScript apps, put shared domain and request/response types in src/lib/types.ts and import the same types from page and route modules. Use imported helpers and export every function consumed by another module. Decide one JSON response shape per operation (bare array, {items}, or {item}) and make the UI read exactly what the route returns. Check Response.ok before accepting a mutation; return errors as {error, details?} and display them in the UI. Connect each requested UI action through its API to persistence and reload the committed result.\n- For server-side file persistence, initialize only a missing file (ENOENT); distinguish valid empty data from missing data. Parse errors, directory replacement and other read or write I/O failures must return 5xx without overwriting any existing or related data. Serialize the complete read-modify-write transaction across all writers (including multiple processes), or use generation checks with conflict handling; write to a temporary file and atomically rename it. Atomic rename alone does not prevent lost updates.\n- Validate the complete proposed state before committing: aggregate quantities by entity key before checking availability, scope totals to the requested period, define rejection/cascade/reassignment for referenced parent deletion, and match strict/inclusive comparisons to the goal wording. Reject non-finite or invalid quantities and amounts at the API boundary.\n\nProfile runtime contract:\n- Preserve the workspace as a real Next.js app.\n- Keep next/react/react-dom dependencies in package.json.\n- Keep scripts.build as next build; do not replace it with echo/skip/no-op commands.\n- If npm run build cannot run because dependencies are not installed, report dependency_missing or use an explicit setup step; do not fake success.\n- Keep scripts.dev and scripts.start on the explicitly requested port 60302: next dev/start -p 60302 or --port 60302.\n- If using Tailwind utility classes or @tailwind directives, keep the Tailwind toolchain complete: tailwindcss/postcss/autoprefixer dependencies, tailwind.config.*, and postcss.config plugins with BOTH tailwindcss and autoprefixer. Otherwise use plain CSS.\n- Keep TypeScript and app router configuration coherent.\n- Keep a single route-bound implementation; do not leave capability components unimported.\n- For interactive UI, expose data-anvil-action=\"primary\" on the main start/submit/action control, data-anvil-action=\"input\" on the main text entry surface when one exists, and data-anvil-state with a JSON snapshot of meaningful visible state after each render. When the contract includes start_or_restart_flow, every restart affordance (including terminal and active states when present) should carry data-anvil-action=\"restart\"; the initial primary action alone cannot satisfy recovery verification. A restart reachable during active use (hook or keyboard shortcut) allows behavioral verification, while an overlay-only restart may verify as unverified:terminal_state_not_reached (partial).\n- Do not treat scaffold-only, package-only, or build-only output as complete.\nRoute-bound implementation constraint:\n- Keep a single route-bound implementation; do not leave capability components unimported.\n\nPre-provisioned scaffold note:\n- Required scaffold files are authored before phase 1 when absent; verify or extend the scaffold rather than re-planning file creation.\nDeterministic verification preference:\n- npm run build\n\n\nRequired final artifacts:\n- package.json\n- tsconfig.json\n- postcss.config.js\n- tailwind.config.ts\n- src/app/layout.tsx\n- src/app/page.tsx\n- src/app/globals.css\n- src/app/global.d.ts\n\nRequired final capabilities:\n- stateful_interaction\n- user_input_or_action\n- visible_state_change\n- persistence\n\nRequired final evidence:\n- nextjs_route_evidence\n- build_command_or_dependency_missing_boundary\n- implementation_artifact\n- visible_interactive_surface_evidence\n- user_input_handler_evidence\n- stateful_update_evidence\n- interactive_ui_source_evidence\n- non_static_screen_evidence\n- persistence_evidence", + "steps": [ + { + "expected_paths": [], + "expected_result": "pass", + "id": "inspect-workspace", + "instruction": "Read existing package.json, tsconfig.json, postcss.config.js, tailwind.config.ts, src/app/layout.tsx, src/app/page.tsx, src/app/globals.css, and src/app/global.d.ts to understand the pre-provisioned scaffold structure, dependency versions, and Tailwind/PostCSS configuration before implementing.", + "kind": "inspect", + "verify": [] + }, + { + "expected_paths": [ + "src/lib/types.ts", + "src/lib/store.ts" + ], + "expected_result": "pass", + "id": "create-types-and-store", + "instruction": "Create src/lib/types.ts exporting domain types: Project (id, name, createdAt), Task (id, projectId, title, deadline, status: 'not-started'|'in-progress'|'completed', assignedMemberId, createdAt), Member (id, name), StatusFilter ('all'|'not-started'|'in-progress'|'completed'), and request/response types (CreateProjectRequest, CreateTaskRequest, CreateMemberRequest, ProjectResponse, TaskResponse, MemberResponse, ErrorResponse with error and optional details). Create src/lib/store.ts exporting functions loadProjects, saveProjects, loadTasks, saveTasks, loadMembers, saveMembers that read/write JSON files under src/data/ (projects.json, tasks.json, members.json). Each load function initialises only on ENOENT (missing file) with an empty array; parse errors or I/O failures throw. Each save function serialises the full array to a temp file then atomically renames it. Use a module-level mutex or generation counter to prevent concurrent lost updates.", + "kind": "implement", + "verify": [ + "test -f src/lib/types.ts", + "test -f src/lib/store.ts" + ] + }, + { + "expected_paths": [ + "src/app/api/projects/route.ts", + "src/app/api/tasks/route.ts", + "src/app/api/members/route.ts" + ], + "expected_result": "pass", + "id": "create-api-routes", + "instruction": "Create three API route modules importing types from src/lib/types.ts and store functions from src/lib/store.ts. 1) src/app/api/projects/route.ts: GET returns {items: Project[]}; POST validates name is non-empty string, creates project with crypto.randomUUID, persists via saveProjects, returns {item: Project} with 201; invalid input returns 400 {error, details}. 2) src/app/api/tasks/route.ts: GET accepts query params projectId and status (StatusFilter), filters tasks accordingly, returns {items: Task[]}; POST validates title, deadline (valid ISO date string), projectId (must exist), assignedMemberId (must exist or empty), status (default 'not-started'), creates task, persists, returns {item: Task} with 201. 3) src/app/api/members/route.ts: GET returns {items: Member[]}; POST validates name, creates member, persists, returns {item: Member} with 201. All error responses use {error, details?} shape. Every function consumed across modules must be exported.", + "kind": "implement", + "verify": [ + "test -f src/app/api/projects/route.ts", + "test -f src/app/api/tasks/route.ts", + "test -f src/app/api/members/route.ts" + ] + }, + { + "expected_paths": [ + "src/app/page.tsx", + "src/app/layout.tsx" + ], + "expected_result": "pass", + "id": "create-page-and-layout", + "instruction": "Rewrite src/app/page.tsx as a client component (\"use client\") that is the single route-bound implementation. It must: (a) import domain types from src/lib/types.ts; (b) render a project creation form with a text input (data-anvil-action=\"input\" on the main name field) and a submit button (data-anvil-action=\"primary\"); (c) render a task creation form with fields for title, deadline (date input), project select, member select, and status; (d) render a status filter dropdown (all, not-started, in-progress, completed) that re-fetches tasks from /api/tasks?status=…; (e) display tasks grouped or listed showing title, deadline, assigned member, and status badge; (f) call fetch to /api/projects, /api/tasks, /api/members on mount and after each mutation; (g) check Response.ok before accepting a mutation response; on !ok parse {error, details?} and display the error message in the UI; (h) set data-anvil-state on the root div to a JSON string snapshot including currentProjects, currentTasks, currentMembers, currentFilter, and any errorMessage after each render; (i) connect create-project, create-task, and filter actions through their API endpoints to persistence and reload committed results. Rewrite src/app/layout.tsx to keep the existing Tailwind metadata and globals.css import, update the html lang to 'ja', and set the title to 'プロジェクト管理'. Ensure no capability component is left unimported; all interactive logic lives in page.tsx.", + "kind": "implement", + "verify": [ + "test -f src/app/page.tsx", + "test -f src/app/layout.tsx" + ] + }, + { + "expected_paths": [ + "package.json" + ], + "expected_result": "pass", + "id": "configure-port-and-scripts", + "instruction": "Update package.json scripts so that dev is 'next dev -p 60302' and start is 'next start -p 60302'. Keep scripts.build as 'next build'. Ensure next, react, react-dom dependencies are present and tailwindcss, postcss, autoprefixer are present. Do not modify tsconfig.json unless it has moduleResolution=node10 or target=ES5—in that case set moduleResolution to 'bundler' and target to 'ES2017'. Keep postcss.config.js plugins including both tailwindcss and autoprefixer. Keep tailwind.config.ts coherent with content paths covering src/app/**/*.tsx.", + "kind": "implement", + "verify": [ + "node -p \"require('./package.json').scripts.dev\"" + ] + }, + { + "expected_paths": [], + "expected_result": "pass", + "id": "build-verification", + "instruction": "Run npm run build to verify the full Next.js TypeScript application compiles, all API routes type-check, the client component in src/app/page.tsx imports types from src/lib/types.ts and helpers from src/lib/store.ts without errors, and no unimported capability modules remain. The build must succeed with exit code 0.", + "kind": "verify", + "verify": [ + "npm run build" + ] + } + ] + } + }, + "source_file": "/var/folders/07/jbhw26n10nj_59d4yp7g5lx80000gn/T/.tmpIIv5TR/.commandagent/runs/issue466/events.jsonl" + } + } + }, + "evidence_projection": "Selected immutable obligation, replacement and boundary fields from two actual product events. Duplicate proposal payloads and the raw event stream are not retained." +} diff --git a/dev-reports/issue-484/implementation-summary.md b/dev-reports/issue-484/implementation-summary.md new file mode 100644 index 00000000..5810e2ad --- /dev/null +++ b/dev-reports/issue-484/implementation-summary.md @@ -0,0 +1,85 @@ +# Issue #484 implementation + +The saved `node -p "require('./package.json').scripts.dev"` now has an admitted +preclosure refinement based on the original owner's literal `next dev -p 60302`. +The real runner rejects the weak original, then accepts the saved complete +proposal on attempt two. It uses no live model or current artifact to acquire +the expectation. + +## Change + +- `package_script_check.rs` recognizes one root-package/static-script grammar. + The refinement reads the same value, uses strict string equality, then prints + the observed value. Its bounded ASCII literal grammar excludes shell/JS + escapes, expansions, operators and whitespace that normalization would alter. +- `package_script_formation.rs` captures model and host source plans before + sanitization, with source file, JSON pointers, owner, acquisition stage and + SHA-256 hashes. It retains the original commands and registered contract + separately. Fixed declarations are parsed as a complete finite grammar; + unknown trailing language is refused, including overrides that omit the key. + The known saved configuration suffix is supported explicitly. Same-key equal + literals agree; conflicting literals or host/registered constraints refuse. + Host port-only guidance restricts an existing literal and cannot supply one. +- The existing equivalence projection admits only the exact output-preserving + replacement. Additive event fields record the original/replacement commands, + hashes, sources, expectation and formed plan. Raw wrapper proposals remain + refused even if the general shell normalizer could strip their fallback. +- The shared command policy's existing setup/server predicate was extracted to + `verify/setup_command_policy.rs`, reducing `verify.rs` within its existing + growth budget. It recognizes this complete comparison so the string + `next dev` is not mistaken for launching a server. Actual server commands and + unsupported Node code retain the existing policy checks. + +## Complete owner preservation + +The saved model instruction is 547 characters and the host instruction is +2,237 characters. Combining them exceeds the unchanged 2,500-character limit. +The saved valid proposal therefore retains both verbatim in two ordered +Implement/pass package writers and executes the complete original check group +after both. `package_owner_scope.rs` proves this bounded split before the +existing model/host preservation checks: exact source text and model identity, +new unique host ID, exact normalized package scope, no third writer, original +host verify empty, and the complete check group after both writers. + +Lint recognizes only this concrete ordered package split, with the full known +host guidance, exact literal comparison and all writer checks after both. +Other duplicate ownership remains rejected. Admission independently matches +the immutable source duties. Preset/runtime setup optimizations preserve these +owners and observations; passing package prechecks cannot short-circuit either +owner's work. The instruction limit, planner/Recovery budgets, existing growth +baselines and acceptance/evidence requirements are unchanged. + +## Tests and evidence + +`tests/corpus/apps/issue484-package-script-formation/` contains the exact saved +proposal/scope/source snapshots, their historical source manifest, the complete +formed proposal, and refusal controls. `formation-evidence.json` records selected +actual admission events and source mappings from the offline runner replay. +Hashes over source plans/steps are compact serde-serialized object hashes; +the source manifest separately records historical file and fixture byte hashes. + +Focused tests cover real Node stdout/exit behavior; absent script/object/file, +invalid JSON and value mismatch; unsafe literals; unknown/conflicting sources; +changed target, result or expected literal; removed owners/duties/checks; +wrappers, catch and output suppression; registered contract conflicts and +unchanged frozen contract bytes/hash. The comparison is `StaticSyntax` and +cannot supply business Test or implementation-artifact credit. + +The parent design and owner-split feedback is incorporated. `owner-refusals.json` and the +owner tests cover text/result/scope changes, writer deletion, third/aliased or +non-normalized writers, ID reuse, checks before/between writers, partial checks, +and nonempty original host verify. #478 duty preservation, #479 pure-import and +fixed diagnostics, #480 execution predicates, and genuine Test classification +remain regression checks. + +The final code-review follow-up is covered by `issue484_capture_tests.rs`. +Pending sources follow the current proposal's augmentation stage until its first +retention, including Ready-then-lint-retry paths; retention freezes that matching +source. Double strengthening without a check is refused explicitly. Source +pointers continue to resolve into the emitted immutable snapshots, and the +positive runner test reaches saved-source Admission and lint, rather than treating +the standalone lint predicate as final acceptance. + +PR, exact-commit CI/UAT, integration and release version/hash are parent work. +No push, PR, Issue mutation, service operation, live model probe, historical +evidence rewrite, runtime namespace change or other worktree edit was performed. diff --git a/dev-reports/issue-484/verification.md b/dev-reports/issue-484/verification.md new file mode 100644 index 00000000..f56f0d2d --- /dev/null +++ b/dev-reports/issue-484/verification.md @@ -0,0 +1,72 @@ +# Issue #484 verification + +- Status: `passed` + +All required worker checks passed on the final implementation. The full Cargo +suite completed outside the sandbox with exit 0, including integration and +doc-tests; its library suite passed 2,609 tests with the existing 19 ignored. + +## Checks + +- `cargo test issue484 --lib`: `passed` +- `cargo test issue478 --lib`: `passed` +- `cargo test issue479 --lib`: `passed` +- `cargo test --test corpus_regression`: `passed` +- `cargo test --test generality_guardrails`: `passed` +- `cargo test template_owned_prompt_feedback_rejects_mixed_final_acceptance_scope --lib`: `passed` +- `cargo test child_that_responds_500_reports_http_failure --lib`: `passed` +- `cargo fmt --all -- --check`: `passed` +- `cargo clippy --all-targets -- -D warnings`: `passed` +- `cargo test`: `passed` +- `git diff --check`: `passed` + +## Evidence and scope + +Baseline/fetched develop: `b7b65f8fd75793f2075491889c66372b02a06d14`. +Node: `v24.1.0`. Final focused #484 suite: 13 passing tests. #478: 7; +#479/#480-related regressions: 21; corpus: 7; guardrails: 10. +The new corpus expectation deliberately rejects implementation-artifact credit +for a passing package-only comparison. + +The saved runner replay rejects the weak original on attempt one and admits the +complete literal comparison and ordered owner split on attempt two. Real Node +checks preserve the matching stdout exactly and fail nonzero with no success +stdout for missing script/object, mismatch, absent file and invalid JSON. An +absent package file retains the existing product `DependencyMissing` report +classification; raw Node and the product both fail. No failure is accepted as +success. + +The design and owner-split follow-ups are covered by unsafe-character and +per-key conflict controls, stdout/exit controls, and the table-driven source, +writer, alias, ID and ordering refusals. The code-review follow-up is covered by +the Ready-then-lint-retry acquisition test and explicit duplicate-strengthen +refusal. Retained snapshots match the proposal first retained, then remain fixed. + +`formation-evidence.json` contains selected fields from actual product events, +not the raw event stream. `initial_rejection.event` and `planner_attempt` locate +the acquisition event; `validated.package_script_formation.sources.source_file` +identifies its stream. Each Source JSON pointer resolves relative to `validated` +into the embedded immutable snapshot; tests recompute its step/instruction hashes. +`source-manifest.json` separately pins the historical file SHA-256 and pointer. +Missing source evidence is never replenished from artifacts or later proposals. +Standalone ordered-split lint is not final acceptance: the positive test passes +real runner Admission against saved sources and then lint. + +## Earlier attempts + +The initial sandbox full run could not bind local HTTP mocks (`Operation not +permitted`); its waiting test process was stopped and the same suite rerun +outside the sandbox. An early outside run exposed a template-prompt regression, +which was narrowed to preserve the old behavior. Its HTTP-500 mock also failed +once under concurrent execution and passed the focused rerun. Integration guards +then identified growth/profile-literal violations; the existing setup predicate +was extracted to a leaf and profile access uses the existing boundary APIs and +identifiers. No guard baseline, timeout, attempt budget or evidence gate was +raised. The final full run passed with no failures; existing ignored tests were not +reclassified or enabled. + +## Parent-only stages + +PR, exact-HEAD CI/UAT, integration and integrated release version/hash: **not run +by this worker; pending parent execution**. They are not included in the worker +passed verdict. No push, PR, Issue mutation or live model reevaluation occurred. diff --git a/src/minimal_loop/evidence.rs b/src/minimal_loop/evidence.rs index 0c5537e1..745775ba 100644 --- a/src/minimal_loop/evidence.rs +++ b/src/minimal_loop/evidence.rs @@ -4,6 +4,7 @@ pub(crate) use artifact_verify::is_artifact_only_verify_command; #[path = "evidence/verify_command_classification.rs"] mod verify_command_classification; pub(crate) use verify_command_classification::import_check; +pub(crate) use verify_command_classification::package_script_check; #[path = "evidence/command_diagnosis.rs"] pub(crate) mod command_diagnosis; #[path = "evidence/weak_sources.rs"] diff --git a/src/minimal_loop/evidence/verify_command_classification.rs b/src/minimal_loop/evidence/verify_command_classification.rs index 4a5c120b..5ac5f113 100644 --- a/src/minimal_loop/evidence/verify_command_classification.rs +++ b/src/minimal_loop/evidence/verify_command_classification.rs @@ -6,6 +6,8 @@ use super::{VerifyCommandKind, WorkspaceEvidence, has_test_artifact}; pub(crate) mod import_check; #[path = "verify_command_classification/node_checks.rs"] mod node_checks; +#[path = "verify_command_classification/package_script_check.rs"] +pub(crate) mod package_script_check; #[path = "verify_command_classification/structural_checks.rs"] mod structural_checks; @@ -44,7 +46,9 @@ pub(super) fn node_command_kind( if crate::planner::profiles::nextjs::recovery_authority::is_generated_hook_check(command) { return Some(VerifyCommandKind::StaticSyntax); } - if structural_checks::recognizes(command) || import_check::export_set_target(command).is_some() + if structural_checks::recognizes(command) + || import_check::export_set_target(command).is_some() + || package_script_check::comparison(command).is_some() { return Some(VerifyCommandKind::StaticSyntax); } diff --git a/src/minimal_loop/evidence/verify_command_classification/package_script_check.rs b/src/minimal_loop/evidence/verify_command_classification/package_script_check.rs new file mode 100644 index 00000000..da97ced0 --- /dev/null +++ b/src/minimal_loop/evidence/verify_command_classification/package_script_check.rs @@ -0,0 +1,48 @@ +//! Closed package-value observation/refinement grammar. These are executed +//! structural checks, never business assertions. No JavaScript equivalence guess. +use super::{import_check, single_command_words}; + +pub(crate) fn identifier(value: &str) -> bool { + value.starts_with(|c: char| c.is_ascii_alphabetic() || c == '_') + && value.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') + && !matches!(value, "constructor" | "prototype" | "__proto__") +} + +pub(crate) fn literal(value: &str) -> bool { + !value.is_empty() + && value.trim() == value + && !value.contains(" ") + && value + .bytes() + .all(|b| b.is_ascii_alphanumeric() || b" _./:=@+-".contains(&b)) +} + +pub(crate) fn printed_script(command: &str) -> Option { + let words = single_command_words(command)?; + let [program, flag, source] = words.as_slice() else { + return None; + }; + let script = source.strip_prefix("require('./package.json').scripts.")?; + (program == "node" && matches!(flag.as_str(), "-p" | "--print") && identifier(script)) + .then(|| script.into()) +} + +pub(crate) fn comparison(command: &str) -> Option<(String, String)> { + let source = import_check::source(command)?; + let rest = source.strip_prefix("const actual=require('./package.json').scripts.")?; + let (script, rest) = rest.split_once(";require('node:assert/strict').strictEqual(actual,")?; + let expected: String = + serde_json::from_str(rest.strip_suffix(");console.log(actual)")?).ok()?; + (identifier(script) && literal(&expected)).then(|| (script.into(), expected)) +} + +pub(crate) fn formed_command(script: &str, expected: &str) -> String { + // Both inputs have already passed the bounded identifier/literal grammar; + // JSON quotes are escaped for the outer shell double quotes. + let value = serde_json::to_string(expected) + .expect("string serialization") + .replace('"', "\\\""); + format!( + "node -e \"const actual=require('./package.json').scripts.{script};require('node:assert/strict').strictEqual(actual,{value});console.log(actual)\"" + ) +} diff --git a/src/planner/lint.rs b/src/planner/lint.rs index ca340963..a34be0d8 100644 --- a/src/planner/lint.rs +++ b/src/planner/lint.rs @@ -226,6 +226,9 @@ fn lint_report_internal(plan: &StepPlan, work_root: Option<&Path>) -> PlanLintRe } if step.step_kind() != StepKind::Verify && let Some(owner) = path_owners.insert(path.as_str(), step.id.as_str()) + && !crate::planner::recovery_step_plan_binding::package_owner_scope::ordered_split( + plan, path, owner, &step.id, + ) { report.push( "path_ownership", diff --git a/src/planner/recovery_step_plan_binding.rs b/src/planner/recovery_step_plan_binding.rs index 4a820bb3..6cc7945d 100644 --- a/src/planner/recovery_step_plan_binding.rs +++ b/src/planner/recovery_step_plan_binding.rs @@ -12,6 +12,8 @@ pub(crate) mod admission; pub(crate) mod formation_scope; #[cfg(test)] mod issue466_tests; +pub(crate) mod package_owner_scope; +mod package_script_formation; pub(crate) mod profile_augmentation; mod verifier_formation; diff --git a/src/planner/recovery_step_plan_binding/admission.rs b/src/planner/recovery_step_plan_binding/admission.rs index ab643b53..20bec8c2 100644 --- a/src/planner/recovery_step_plan_binding/admission.rs +++ b/src/planner/recovery_step_plan_binding/admission.rs @@ -12,6 +12,10 @@ pub(crate) struct Admission { sources: Option, profile_addition: Option, replacements: Vec, + pending_sources: Option, + package_scripts: Option, + awaiting_check: bool, + duplicate_strengthen: bool, } pub(crate) enum Decision { @@ -21,14 +25,27 @@ pub(crate) enum Decision { impl Admission { pub(crate) fn strengthen(&mut self, config: &Config, plan: &mut StepPlan) { + if self.awaiting_check { + self.duplicate_strengthen = true; + return; + } + self.awaiting_check = true; self.profile_addition = super::profile_augmentation::strengthen_step_plan_for_profile(plan, config); + if self.original.is_none() { + self.pending_sources = Some(super::package_script_formation::CapturedSources::new( + config, + FormationScope::capture(plan, self.profile_addition.as_ref()), + "profile_augmentation_before_sanitization", + )); + } } fn preserve(&self, plan: &StepPlan) -> anyhow::Result<()> { super::verifier_formation::preserve_registered(&self.replacements, plan)?; if let Some(original) = &self.original { - let (projected, _) = super::verifier_formation::project(original, plan); + let (projected, _) = + super::verifier_formation::project(original, plan, self.package_scripts.as_ref()); if let Some(sources) = &self.sources { sources.preserve(original, &projected)?; } else { @@ -38,12 +55,22 @@ impl Admission { Ok(()) } - fn retain(&mut self, plan: &StepPlan) { + fn retain(&mut self, config: &Config, plan: &StepPlan, contract: &CompletionContract) { if self.original.is_none() { self.sources = Some(FormationScope::capture( plan, self.profile_addition.as_ref(), )); + let sources = self.pending_sources.take().unwrap_or_else(|| { + super::package_script_formation::CapturedSources::new( + config, + self.sources.as_ref().expect("captured sources").clone(), + "admission_before_first_retry", + ) + }); + self.package_scripts = Some(super::package_script_formation::PackageScripts::capture( + config, plan, sources, contract, + )); self.original = Some(plan.clone()); } } @@ -54,6 +81,7 @@ impl Admission { phase: Option<&str>, plan: StepPlan, ) -> anyhow::Result { + self.require_capture_boundary()?; if let Some(original) = &self.original && let Err(error) = self.preserve(&plan) { @@ -91,13 +119,20 @@ impl Admission { plan: &mut StepPlan, attempt: usize, ) -> anyhow::Result { + self.awaiting_check = false; let host = plan.clone(); let recovery = crate::planner::recovery_inspection::has_origin(config)?; - let result = if recovery { + let result = if let Err(error) = self.require_capture_boundary() { + Err(error) + } else if recovery { check_model_ownership(config, phase, model) .and_then(|()| bind_generated(config, phase, plan)) } else { - self.form(config, plan).map(|()| false) + self.package_scripts + .as_ref() + .map_or(Ok(()), |p| p.preserve_raw_commands(model)) + .and_then(|()| self.form(config, plan)) + .map(|()| false) }; let Err(error) = result else { return Ok(Decision::Ready(result.unwrap())); @@ -115,6 +150,7 @@ impl Admission { "classification":class, "reason":error.to_string(), "model_proposal":model, "host_augmented_proposal":host, "original_scope":self.original, "original_obligation_sources":self.sources, + "package_script_formation":self.package_scripts, "planner_attempt":attempt, "remaining_planner_attempts":3usize.saturating_sub(attempt), "recovery_budget_changed":false, @@ -141,6 +177,16 @@ impl Admission { ))) } + fn require_capture_boundary(&self) -> anyhow::Result<()> { + if self.duplicate_strengthen { + return Err(failure( + FailureClass::Unsafe, + "source acquisition requires one strengthen followed by check per proposal; duplicate strengthen cannot identify the original source", + )); + } + Ok(()) + } + fn form(&mut self, config: &Config, plan: &StepPlan) -> anyhow::Result<()> { if !matches!( config.profile.as_str(), @@ -152,7 +198,8 @@ impl Admission { } self.preserve(plan)?; if let Some(original) = &self.original { - let (_, replacements) = super::verifier_formation::project(original, plan); + let (_, replacements) = + super::verifier_formation::project(original, plan, self.package_scripts.as_ref()); for replacement in replacements { if !self.replacements.contains(&replacement) { self.replacements.push(replacement); @@ -170,21 +217,35 @@ impl Admission { let Ok(admitted) = scope::admitted_commands(config, &contract, plan) else { return Ok(()); }; + let registered_contract = contract.clone(); contract.verify_commands.extend(admitted); if let Err(error) = super::verifier_formation::require_formed(config, &scope::commands(plan)) { - self.retain(plan); - return Err(error); + self.retain(config, plan, ®istered_contract); + let guidance = self + .package_scripts + .as_ref() + .map(|p| p.guidance()) + .unwrap_or_default(); + return if guidance.is_empty() { + Err(error) + } else { + let message = format!("{error}\n{guidance}"); + Err(error.context(message)) + }; } if let Some(original) = &self.original { - let (_, replacements) = super::verifier_formation::project(original, plan); + let (_, replacements) = + super::verifier_formation::project(original, plan, self.package_scripts.as_ref()); crate::eval_events::emit( config.eval_events_path.as_deref(), json!({ "event":"preclosure_verifier_replacements_validated", "replacements":replacements, "scope_preserved":true, "formed_verify_commands":scope::commands(plan), + "package_script_formation":self.package_scripts, + "formed_plan":plan, }), ); } @@ -195,7 +256,7 @@ impl Admission { if let Some(addition) = &self.profile_addition && let Err(error) = addition.preserve(plan) { - self.retain(plan); + self.retain(config, plan, ®istered_contract); return Err(error); } let mut claimed = Vec::<&PlanStep>::new(); @@ -211,11 +272,11 @@ impl Admission { scope::checked_paths(config, &contract, step) .map_err(|e| failure(FailureClass::Unsafe, e.to_string()))?; if let Err(error) = scope::validate_producer(config, &contract, step) { - self.retain(plan); + self.retain(config, plan, ®istered_contract); return Err(failure(FailureClass::ProposalRepairable, error.to_string())); } if claimed.iter().any(|old| scope::overlaps(old, step)) { - self.retain(plan); + self.retain(config, plan, ®istered_contract); return Err(failure( FailureClass::ProposalRepairable, format!( diff --git a/src/planner/recovery_step_plan_binding/formation_scope.rs b/src/planner/recovery_step_plan_binding/formation_scope.rs index 235a7cf5..ebefb092 100644 --- a/src/planner/recovery_step_plan_binding/formation_scope.rs +++ b/src/planner/recovery_step_plan_binding/formation_scope.rs @@ -44,10 +44,10 @@ impl ProfileAddition { } } -#[derive(serde::Serialize)] +#[derive(Clone, serde::Serialize)] pub(crate) struct FormationScope { - model: StepPlan, - host: StepPlan, + pub(super) model: StepPlan, + pub(super) host: StepPlan, } impl FormationScope { @@ -69,8 +69,15 @@ impl FormationScope { } pub(crate) fn preserve(&self, original: &StepPlan, proposed: &StepPlan) -> anyhow::Result<()> { - super::admission::preserve(&self.model, proposed)?; - super::admission::preserve(&self.host, proposed)?; + if let Some((model_view, host_view)) = + super::package_owner_scope::views(&self.model, &self.host, original, proposed) + { + super::admission::preserve(&self.model, &model_view)?; + super::admission::preserve(&self.host, &host_view)?; + } else { + super::admission::preserve(&self.model, proposed)?; + super::admission::preserve(&self.host, proposed)?; + } preserve_boundaries(original, proposed) } } diff --git a/src/planner/recovery_step_plan_binding/issue466_tests.rs b/src/planner/recovery_step_plan_binding/issue466_tests.rs index 200a43a0..49ba14bf 100644 --- a/src/planner/recovery_step_plan_binding/issue466_tests.rs +++ b/src/planner/recovery_step_plan_binding/issue466_tests.rs @@ -165,3 +165,6 @@ mod issue478; #[path = "issue479_tests.rs"] mod issue479; + +#[path = "issue484_tests.rs"] +mod issue484; diff --git a/src/planner/recovery_step_plan_binding/issue484_capture_tests.rs b/src/planner/recovery_step_plan_binding/issue484_capture_tests.rs new file mode 100644 index 00000000..927cc9ad --- /dev/null +++ b/src/planner/recovery_step_plan_binding/issue484_capture_tests.rs @@ -0,0 +1,70 @@ +use super::*; + +#[test] +fn issue484_capture_tracks_retained_proposal_after_ready_then_lint_retry() { + let root = tempfile::tempdir().unwrap(); + let c = config(root.path()); + let mut admission = admission::Admission::default(); + let mut first = single_owner("Update package.json scripts so that dev is 'next dev -p 1111'."); + first.steps[0].id.clear(); + first.steps[0].verify = vec!["test -f package.json".into()]; + let raw = first.clone(); + admission.strengthen(&c, &mut first); + assert!(matches!( + admission.check(&c, None, &raw, &mut first, 1).unwrap(), + admission::Decision::Ready(false) + )); + assert!(!crate::planner::lint::lint_plan_for_execution(&first, Some(root.path())).is_pass()); + + let raw = single_owner(&saved(false).steps[4].instruction); + let mut second = raw.clone(); + admission.strengthen(&c, &mut second); + assert!(matches!( + admission.check(&c, None, &raw, &mut second, 2).unwrap(), + admission::Decision::Retry(_) + )); + let retained = events(&c) + .into_iter() + .find(|e| e["event"] == "recovery_verifier_plan_admission") + .unwrap(); + assert_eq!(retained["planner_attempt"], 2); + let formation = &retained["package_script_formation"]; + assert_eq!( + formation["obligations"][0]["expected_literal"], + "next dev -p 60302" + ); + assert_eq!(formation["sources"]["plans"]["model"], json!(raw)); + assert_eq!(retained["original_scope"], json!(second)); + + let mut third = single_owner("Update package.json scripts so that dev is 'next dev -p 3000'."); + third.steps[0].verify = vec![check().replace("60302", "3000")]; + let raw = third.clone(); + admission.strengthen(&c, &mut third); + assert!(admission.check(&c, None, &raw, &mut third, 3).is_err()); + let last = events(&c) + .into_iter() + .rfind(|e| e["event"] == "recovery_verifier_plan_admission") + .unwrap(); + assert_eq!( + last["package_script_formation"], *formation, + "retain must freeze the matching second proposal, not first or third" + ); + assert!(admission.finish(&c, None, third).is_err()); +} + +#[test] +fn issue484_double_strengthen_without_check_is_an_explicit_caller_failure() { + let root = tempfile::tempdir().unwrap(); + let c = config(root.path()); + let mut admission = admission::Admission::default(); + let mut candidate = single_owner(&saved(false).steps[4].instruction); + let raw = candidate.clone(); + admission.strengthen(&c, &mut candidate); + admission.strengthen(&c, &mut candidate); + let error = admission + .check(&c, None, &raw, &mut candidate, 1) + .err() + .unwrap(); + assert!(error.to_string().contains("duplicate strengthen")); + assert!(admission.finish(&c, None, candidate).is_err()); +} diff --git a/src/planner/recovery_step_plan_binding/issue484_owner_tests.rs b/src/planner/recovery_step_plan_binding/issue484_owner_tests.rs new file mode 100644 index 00000000..f9064891 --- /dev/null +++ b/src/planner/recovery_step_plan_binding/issue484_owner_tests.rs @@ -0,0 +1,142 @@ +use super::*; + +fn projected_split(c: &Config) -> (StepPlan, FormationScope, StepPlan) { + let original: StepPlan = serde_json::from_str(&fixture("saved-scope.json")).unwrap(); + let sources: serde_json::Value = + serde_json::from_str(&fixture("saved-obligation-sources.json")).unwrap(); + let sources = FormationScope { + model: serde_json::from_value(sources["model"].clone()).unwrap(), + host: serde_json::from_value(sources["host"].clone()).unwrap(), + }; + let mut candidate = saved(true); + crate::planner::step_plan::repair_generated_step_plan_contract(&mut candidate); + // Run the real preset transformation, then project the already-proven + // comparison to the original command for scope/boundary checks. + let runtime = crate::planner::profile::resolve_profile_runtime( + crate::planner::profile_descriptor::NEXTJS_PROFILE_ID, + ); + runtime.convert_preset_phase_setup_steps( + &mut candidate, + &c.workspace_root, + &saved(false).goal, + Some(("core-implementation", false)), + true, + None, + ); + candidate.steps[6].verify[0] = saved(false).steps[4].verify[0].clone(); + (original, sources, candidate) +} + +#[test] +fn issue484_split_rejects_every_source_writer_and_boundary_loss() { + let root = tempfile::tempdir().unwrap(); + let c = super::super::issue478::nextjs_config(root.path()); + let (original, sources, candidate) = projected_split(&c); + sources.preserve(&original, &candidate).unwrap(); + let cases: Vec = serde_json::from_str(&fixture("owner-refusals.json")).unwrap(); + for case in cases { + let mut p = candidate.clone(); + let mut s = sources.clone(); + match case.as_str() { + "model_text" => p.steps[4].instruction.push_str(" Changed duty."), + "host_text" => p.steps[5].instruction.pop().map(|_| ()).unwrap(), + "model_result" => p.steps[4].expected_result = "fail".into(), + "host_result" => p.steps[5].expected_result = "fail".into(), + "model_scope" => p.steps[4].expected_paths = vec!["other.json".into()], + "host_scope" => p.steps[5].expected_paths = vec!["other.json".into()], + "delete_model" => { + p.steps.remove(4); + } + "delete_host" => { + p.steps.remove(5); + } + "third_writer" | "alias_third_writer" | "non_normalized_writer" => { + let mut third = p.steps[5].clone(); + third.id = "third-writer".into(); + third.expected_paths = vec![ + match case.as_str() { + "alias_third_writer" => "./package.json", + "non_normalized_writer" => "config/../package.json", + _ => "package.json", + } + .into(), + ]; + p.steps.insert(6, third); + } + "same_id" => p.steps[5].id = p.steps[4].id.clone(), + "host_before_model" => p.steps.swap(4, 5), + "check_between_writers" => p.steps.swap(5, 6), + "check_before_writers" => p.steps.swap(4, 6), + "partial_checks" => { + p.steps[6].verify.remove(1); + } + "host_source_verify" => s.host.steps[0].verify = vec!["test -f package.json".into()], + "host_source_result" => s.host.steps[0].expected_result = "fail".into(), + _ => unreachable!(), + } + assert!(s.preserve(&original, &p).is_err(), "{case}"); + } +} + +#[test] +fn issue484_lint_requires_complete_ordered_split_and_runtime_keeps_owners_executable() { + let root = tempfile::tempdir().unwrap(); + let c = super::super::issue478::nextjs_config(root.path()); + let (_, _, mut candidate) = projected_split(&c); + candidate.steps[6].verify[0] = check(); + let runtime = crate::planner::profile::resolve_profile_runtime( + crate::planner::profile_descriptor::NEXTJS_PROFILE_ID, + ); + for owner in &candidate.steps[4..6] { + assert_eq!(owner.step_kind(), StepKind::Implement); + assert!(!runtime.step_short_circuit_precheck_applicable(owner)); + let (executed, synthesized) = runtime.runtime_step_with_profile_checks( + root.path(), + &candidate.goal, + owner, + Some("core-implementation"), + None, + ); + assert!(!synthesized); + assert_eq!(executed, *owner); + } + let permits = |p: &StepPlan| { + super::super::super::package_owner_scope::ordered_split( + p, + "package.json", + &p.steps[4].id, + &p.steps[5].id, + ) + }; + assert!(permits(&candidate)); + for case in [ + "wrong_literal", + "no_final_check", + "early_final_check", + "third_writer", + "unknown_host", + "missing_host_check", + ] { + let mut p = candidate.clone(); + match case { + "wrong_literal" => p.steps[6].verify[0] = check().replace("60302", "3000"), + "no_final_check" => p.steps[6].verify.clear(), + "early_final_check" => p.steps.swap(5, 6), + "third_writer" => { + let mut extra = p.steps[5].clone(); + extra.id = "third".into(); + p.steps.push(extra); + } + "unknown_host" => p.steps[5].instruction = "Maintain package.json.".into(), + "missing_host_check" => { + p.steps[6].verify.pop(); + } + _ => unreachable!(), + } + assert!(!permits(&p), "{case}"); + assert!( + !crate::planner::lint::lint_plan_for_execution(&p, Some(root.path())).is_pass(), + "{case}" + ); + } +} diff --git a/src/planner/recovery_step_plan_binding/issue484_runtime_tests.rs b/src/planner/recovery_step_plan_binding/issue484_runtime_tests.rs new file mode 100644 index 00000000..b3b89250 --- /dev/null +++ b/src/planner/recovery_step_plan_binding/issue484_runtime_tests.rs @@ -0,0 +1,249 @@ +use super::*; +use crate::minimal_loop::evidence::command_diagnosis; + +fn execute(root: &Path, command: &str) -> crate::planner::verify::VerificationReport { + let mut step = saved(true).steps[4].clone(); + step.kind = "verify".into(); + step.expected_paths.clear(); + step.verify = vec![command.into()]; + crate::planner::verify::verify_step(root, &step) +} + +fn raw(root: &Path, command: &str) -> crate::bounded_process::BoundedProcessOutput { + crate::bounded_process::run_with_timeout( + std::process::Command::new("sh") + .args(["-c", command]) + .current_dir(root) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()), + std::time::Duration::from_secs(5), + ) + .unwrap() +} + +#[test] +fn issue484_real_node_preserves_stdout_and_propagates_every_runtime_failure() { + let root = tempfile::tempdir().unwrap(); + let original = saved(false).steps[4].verify[0].clone(); + let command = check(); + let package = root.path().join("package.json"); + for (case, data, passes) in [ + ("matching", Some(fixture("package.json")), true), + ("script_missing", Some("{\"scripts\":{}}".into()), false), + ("scripts_missing", Some("{}".into()), false), + ( + "mismatch", + Some(fixture("package.json").replace("60302", "3000")), + false, + ), + ("file_missing", None, false), + ("invalid_json", Some("not JSON".into()), false), + ] { + if let Some(data) = data { + std::fs::write(&package, data).unwrap(); + } else { + std::fs::remove_file(&package).unwrap(); + } + let result = execute(root.path(), &command); + assert_eq!(result.is_pass(), passes, "{case}: {result:?}"); + let actual = raw(root.path(), &command); + assert_eq!(actual.success(), passes, "{case}"); + if passes { + let before = raw(root.path(), &original); + assert!(before.success()); + assert_eq!(actual.stdout, before.stdout); + assert_eq!(actual.stdout, b"next dev -p 60302\n"); + assert!(actual.stderr.is_empty()); + } else { + assert!(actual.stdout.is_empty(), "{case} emitted success output"); + assert!(!actual.stderr.is_empty()); + if case == "file_missing" { + // Existing verifier reporting classifies MODULE_NOT_FOUND as + // DependencyMissing; raw Node still proves the nonzero failure. + assert!(!result.dependency_missing.is_empty(), "{result:?}"); + } else { + assert!( + result.command_failures.iter().any(|f| f.command == command), + "{case}: {result:?}" + ); + } + if matches!(case, "file_missing" | "invalid_json" | "scripts_missing") { + assert!( + !raw(root.path(), &original).success(), + "original failure must also propagate" + ); + } + } + } +} + +#[test] +fn issue484_allowed_literal_characters_survive_shell_and_node_unchanged() { + let root = tempfile::tempdir().unwrap(); + let c = config(root.path()); + // Exercise every accepted punctuation, including JSON-independent shell + // boundaries. Characters outside this ASCII grammar are refusal fixtures. + for value in [ + "next dev --port=60302", + "A_z09 ./bin/run-js --arg=value:name @scope/pkg +ok", + ] { + assert!(grammar::literal(value)); + let before = single_owner(&format!( + "Update package.json scripts so that dev is '{value}'." + )); + let mut after = before.clone(); + let command = grammar::formed_command("dev", value); + after.steps[0].verify = vec![command.clone()]; + assert!(matches!( + direct(&c, &before, &after).0, + admission::Decision::Ready(false) + )); + std::fs::write( + root.path().join("package.json"), + json!({"scripts":{"dev":value}}).to_string(), + ) + .unwrap(); + let result = raw(root.path(), &command); + assert!(result.success(), "{result:?}"); + assert_eq!(result.stdout, format!("{value}\n").as_bytes()); + assert!(execute(root.path(), &command).is_pass()); + } +} + +#[test] +fn issue484_structural_check_cannot_supply_test_or_implementation_credit() { + let root = tempfile::tempdir().unwrap(); + std::fs::write(root.path().join("package.json"), fixture("package.json")).unwrap(); + let command = check(); + assert_eq!( + command_diagnosis::collect(root.path(), std::slice::from_ref(&command))[0].kind, + "static_syntax" + ); + assert_eq!( + command_diagnosis::collect(root.path(), std::slice::from_ref(&command))[0].repairability, + "runtime_input_dependent" + ); + assert!(execute(root.path(), &command).is_pass()); + let contract: CompletionContract = serde_json::from_value(json!({ + "required_paths":["package.json"], "verify_commands":[command], + "required_evidence":["implementation_artifact"] + })) + .unwrap(); + let report = contract.verify_with_goal(root.path(), "Implement application behavior"); + assert!(!report.is_pass(), "{report:?}"); + assert!( + report.primary_reason().contains("implementation_artifact"), + "{report:?}" + ); + // An existing genuine target-dependent business check keeps its Test class. + let business = "node -e \"import('./src/calculate.mjs').then(actual=>{require('node:assert/strict').deepStrictEqual(actual.double(...[21]),42)})\""; + assert_eq!( + command_diagnosis::collect(root.path(), &[business.into()])[0].kind, + "test" + ); +} + +#[test] +fn issue484_registered_contract_conflicts_and_frozen_hash_are_preserved() { + let root = tempfile::tempdir().unwrap(); + let mut c = config(root.path()); + let guard = authority::begin_run(&c); + let path = generated_contract(&c); + let before = single_owner(&saved(false).steps[4].instruction); + let mut after = before.clone(); + after.steps[0].verify = vec![check()]; + std::fs::write( + &path, + json!({"profile":"generic","verify_commands":[check().replace("60302", "3000")]}) + .to_string(), + ) + .unwrap(); + let (decision, admission) = direct(&c, &before, &after); + assert!(matches!(decision, admission::Decision::Retry(_))); + assert!(admission.finish(&c, None, after.clone()).is_err()); + assert!(events(&c).iter().any(|e| { + e["reason"] + .as_str() + .is_some_and(|s| s.contains("registered completion contract")) + })); + + std::fs::write( + &path, + json!({"profile":"generic","verify_commands":[]}).to_string(), + ) + .unwrap(); + let (decision, admission) = direct(&c, &before, &after); + assert!(matches!(decision, admission::Decision::Ready(false))); + let formed = admission.finish(&c, None, after).unwrap(); + scope::register(&c, &formed).unwrap(); + c.completion_contract_path = Some(path.clone()); + let bytes = std::fs::read(&path).unwrap(); + let contract = CompletionContract::load_for_config(&c).unwrap().unwrap(); + assert_eq!(contract.verify_commands, [check()]); + let digest = hash(&bytes); + let mut changed = formed.clone(); + changed.steps[0].verify[0] = check().replace("60302", "3000"); + scope::register(&c, &changed).unwrap(); + assert_eq!(hash(&std::fs::read(&path).unwrap()), digest); + std::fs::write(root.path().join("package.json"), fixture("package.json")).unwrap(); + assert!( + contract + .verify_with_goal(root.path(), "Check package structure") + .is_pass() + ); + std::fs::write( + root.path().join("package.json"), + fixture("package.json").replace("60302", "3000"), + ) + .unwrap(); + let failed = contract.verify_with_goal(root.path(), "Check package structure"); + assert!(!failed.is_pass()); + assert!(failed.command_failures.iter().any(|f| f.command == check())); + assert_eq!(std::fs::read(&path).unwrap(), bytes); + drop(guard); +} + +#[test] +fn issue484_host_port_cannot_supply_literal_or_override_explicit_model_value() { + let root = tempfile::tempdir().unwrap(); + let c = super::issue478::nextjs_config(root.path()); + for (instruction, accepted) in [ + ("Maintain package.json.", false), + ( + "Update package.json scripts so that dev is 'next dev -p 3000'.", + false, + ), + ( + "Update package.json scripts so that dev is 'next dev --port 60302'.", + true, + ), + ] { + let mut before = single_owner(instruction); + before.goal = "Maintain package scripts on port 60302".into(); + let mut admission = admission::Admission::default(); + admission.strengthen(&c, &mut before); + assert!(matches!( + admission + .check(&c, None, &before.clone(), &mut before.clone(), 1) + .unwrap(), + admission::Decision::Retry(_) + )); + let mut after = before.clone(); + after.steps[0].verify = vec![grammar::formed_command( + "dev", + if accepted { + "next dev --port 60302" + } else { + "next dev -p 60302" + }, + )]; + let decision = admission + .check(&c, None, &after.clone(), &mut after, 2) + .unwrap(); + assert_eq!( + matches!(decision, admission::Decision::Ready(false)), + accepted, + "{instruction}" + ); + } +} diff --git a/src/planner/recovery_step_plan_binding/issue484_tests.rs b/src/planner/recovery_step_plan_binding/issue484_tests.rs new file mode 100644 index 00000000..57fe36a3 --- /dev/null +++ b/src/planner/recovery_step_plan_binding/issue484_tests.rs @@ -0,0 +1,370 @@ +use super::super::formation_scope::{FormationScope, ProfileAddition}; +use super::super::package_script_formation::{CapturedSources, PackageScripts, hash}; +use super::*; +use crate::minimal_loop::evidence::package_script_check as grammar; + +fn fixture(name: &str) -> String { + std::fs::read_to_string( + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("tests/corpus/apps/issue484-package-script-formation") + .join(name), + ) + .unwrap() +} + +fn saved(formed: bool) -> StepPlan { + serde_json::from_str(&fixture(if formed { + "formed-plan.json" + } else { + "original-plan.json" + })) + .unwrap() +} + +fn check() -> String { + saved(true).steps[6].verify[0].clone() +} + +fn replay(c: &Config, before: &StepPlan, after: &StepPlan) -> (anyhow::Result, Replay) { + let mut client = Replay::new(vec![proposal(before), proposal(after), proposal(after)]); + let result = crate::planner::runner::generate_step_plan_with_ui_for_phase( + &mut client, + &before.goal, + c, + &crate::tui::NOOP_UI, + Some("core-implementation"), + true, + false, + ); + (result, client) +} + +#[test] +fn issue484_saved_runner_forms_exact_literal_and_records_both_sources() { + let root = tempfile::tempdir().unwrap(); + let c = super::issue478::nextjs_config(root.path()); + let before = saved(false); + let after = saved(true); + // Control for the pre-fix equivalence path: pure-import projection cannot + // discharge this saved value display, even though the candidate is valid. + let (unformed, records) = super::super::verifier_formation::project(&before, &after, None); + assert!(records.is_empty()); + assert!(admission::preserve(&before, &unformed).is_err()); + let (result, client) = replay(&c, &before, &after); + let formed = result.unwrap_or_else(|e| { + let reasons: Vec<_> = events(&c) + .into_iter() + .filter(|event| event["event"] == "recovery_verifier_plan_admission") + .map(|event| event["reason"].clone()) + .collect(); + panic!("{e:#}\n{}", json!(reasons)) + }); + assert_eq!(client.requests.lock().unwrap().len(), 2); + assert!(formed.steps[6].verify.contains(&check())); + let log = events(&c); + let rejected = log + .iter() + .find(|e| e["event"] == "recovery_verifier_plan_admission") + .unwrap(); + assert_eq!(rejected["status"], "retry"); + assert_eq!(rejected["remaining_planner_attempts"], 2); + assert_eq!(rejected["recovery_budget_changed"], false); + let event = log + .iter() + .find(|e| e["event"] == "preclosure_verifier_replacements_validated") + .unwrap(); + let formation = &event["package_script_formation"]; + assert_eq!( + formation["sources"]["acquisition_stage"], + "profile_augmentation_before_sanitization" + ); + let plans = &formation["sources"]["plans"]; + for side in ["model", "host"] { + let plan: StepPlan = serde_json::from_value(plans[side].clone()).unwrap(); + assert_eq!( + formation["sources"][format!("{side}_sha256")], + hash(&serde_json::to_vec(&plan).unwrap()) + ); + } + let obligation = &event["replacements"][0]["package_script"]; + assert_eq!(obligation["original_command"], before.steps[4].verify[0]); + assert_eq!( + obligation["original_command_sha256"], + hash(before.steps[4].verify[0].as_bytes()) + ); + assert_eq!( + obligation["replacement_command_sha256"], + hash(check().as_bytes()) + ); + assert_eq!(obligation["expected_literal"], "next dev -p 60302"); + assert_eq!(obligation["expected_result"], "pass"); + assert_eq!( + obligation["original_check_owner"], + "configure-port-and-scripts" + ); + assert_eq!(obligation["original_step_index"], 4); + let sources = obligation["sources"].as_array().unwrap(); + assert_eq!(sources.len(), 2); + assert_eq!(sources[0]["fixed_literals"], json!(["next dev -p 60302"])); + assert_eq!( + sources[1]["fixed_literals"], + json!([]), + "host port constraint is not a fixed literal" + ); + for source in sources { + let step: PlanStep = serde_json::from_value( + event + .pointer(source["json_pointer"].as_str().unwrap()) + .unwrap() + .clone(), + ) + .unwrap(); + assert_eq!(source["owner"], step.id); + assert_eq!( + source["step_sha256"], + hash(&serde_json::to_vec(&step).unwrap()) + ); + assert_eq!( + source["instruction_sha256"], + hash(step.instruction.as_bytes()) + ); + assert!( + formed + .steps + .iter() + .any(|owner| owner.instruction == step.instruction + && owner.expected_paths == ["package.json"] + && owner.step_kind() == StepKind::Implement) + ); + } + let manifest: serde_json::Value = + serde_json::from_str(&fixture("source-manifest.json")).unwrap(); + for (name, digest) in manifest["fixture_sha256"].as_object().unwrap() { + assert_eq!( + hash(fixture(name).as_bytes()), + digest.as_str().unwrap(), + "{name}" + ); + } + if let Ok(path) = std::env::var("ISSUE484_FORMATION_EVIDENCE") { + std::fs::write( + path, + serde_json::to_vec_pretty(&json!({ + "source_manifest":manifest, "initial_rejection":rejected, + "validated":event, "planner_requests":2, + "execution":"offline saved replay; no live model or application build" + })) + .unwrap(), + ) + .unwrap(); + } +} + +#[test] +fn issue484_saved_runner_refuses_scope_output_failure_and_literal_changes() { + let cases: Vec = serde_json::from_str(&fixture("refusals.json")).unwrap(); + for case in cases { + let root = tempfile::tempdir().unwrap(); + let c = super::issue478::nextjs_config(root.path()); + let before = saved(false); + let mut after = saved(true); + match case.as_str() { + "wrong_target" => { + after.steps[6].verify[0] = check().replace("./package.json", "./other.json") + } + "changed_literal" | "artifact_derived_literal" => { + after.steps[6].verify[0] = check().replace("60302", "3000"); + if case == "artifact_derived_literal" { + std::fs::write( + root.path().join("package.json"), + fixture("package.json").replace("60302", "3000"), + ) + .unwrap(); + after.steps[4].instruction = + after.steps[4].instruction.replace("60302", "3000"); + } + } + "changed_script" => { + after.steps[6].verify[0] = check().replace("scripts.dev", "scripts.start") + } + "delete_check" => after.steps[6].verify.clear(), + "changed_result" => after.steps[4].expected_result = "fail".into(), + "unrelated_assertion" => { + after.steps[6].verify[0] = + "node -e \"require('node:assert/strict').strictEqual(1,1)\"".into() + } + "missing_owner" => { + after.steps.remove(4); + } + "nonexecuting_owner" => after.steps[4].kind = "report".into(), + "premature_check" => { + let mut verification = after.steps[6].clone(); + verification.id = "premature-package-check".into(); + verification.kind = "verify".into(); + verification.expected_paths.clear(); + after.steps[6].verify.clear(); + after.steps.insert(1, verification); + } + "swallow_failure" => after.steps[6].verify[0] = format!("{} || true", check()), + "wrapper" => { + after.steps[6].verify[0] = check() + .replace("const actual=", "try { const actual=") + .replace("console.log(actual)", "console.log(actual) } catch {}") + } + "output_suppression" => { + after.steps[6].verify[0] = check().replace(";console.log(actual)", "") + } + "append_beside_original" => after.steps[4] + .verify + .push(before.steps[4].verify[0].clone()), + "drop_other_key_duty" => { + after.steps[4].instruction = after.steps[4] + .instruction + .replace(" and start is 'next start -p 60302'", "") + } + _ => panic!("{case}"), + } + let (result, client) = replay(&c, &before, &after); + assert!(result.is_err(), "{case} was admitted"); + assert_eq!(client.requests.lock().unwrap().len(), 3, "{case}"); + } +} + +fn single_owner(instruction: &str) -> StepPlan { + let mut p = plan(vec![saved(false).steps[4].clone()]); + p.steps[0].instruction = instruction.into(); + p +} + +fn direct( + c: &Config, + before: &StepPlan, + after: &StepPlan, +) -> (admission::Decision, admission::Admission) { + let mut admission = admission::Admission::default(); + let first = admission + .check(c, None, before, &mut before.clone(), 1) + .unwrap(); + assert!(matches!(first, admission::Decision::Retry(_))); + ( + admission + .check(c, None, after, &mut after.clone(), 2) + .unwrap(), + admission, + ) +} + +#[test] +fn issue484_unsupported_commands_and_unsafe_literals_never_gain_admission() { + let controls: serde_json::Value = + serde_json::from_str(&fixture("unsupported-sources.json")).unwrap(); + let root = tempfile::tempdir().unwrap(); + let c = config(root.path()); + for command in controls["commands"].as_array().unwrap() { + let mut before = single_owner(&saved(false).steps[4].instruction); + before.steps[0].verify = vec![command.as_str().unwrap().into()]; + let mut after = before.clone(); + after.steps[0].verify = vec![check()]; + assert!( + matches!(direct(&c, &before, &after).0, admission::Decision::Retry(_)), + "{command}" + ); + } + for value in controls["unsafe_literals"].as_array().unwrap() { + let value = value.as_str().unwrap(); + assert!(!grammar::literal(value)); + let before = single_owner(&format!( + "Update package.json scripts so that dev is '{value}'." + )); + let mut after = before.clone(); + after.steps[0].verify = vec![check()]; + let (decision, admission) = direct(&c, &before, &after); + assert!( + matches!(decision, admission::Decision::Retry(_)), + "{value:?}" + ); + assert!(admission.finish(&c, None, after).is_err()); + } + for instruction in [ + "Maintain package.json.", + "Update package.json scripts so that dev is 'next dev -p 60302' or 'next dev --port 60302'.", + "Update package.json scripts so that dev is 'next dev -p 60302'. Or dev may use port 3000.", + "Update package.json scripts so that dev is 'next dev -p 60302'. Or use any port.", + "Set dev to the currently generated value.", + ] { + let before = single_owner(instruction); + let mut after = before.clone(); + after.steps[0].verify = vec![check()]; + assert!( + matches!(direct(&c, &before, &after).0, admission::Decision::Retry(_)), + "{instruction}" + ); + } +} + +#[test] +fn issue484_model_host_literals_are_checked_per_key_without_precedence() { + let root = tempfile::tempdir().unwrap(); + let c = config(root.path()); + let before = single_owner( + "Update package.json scripts so that dev is 'next dev -p 60302' and start is 'next start -p 60302'.", + ); + let empty: CompletionContract = serde_json::from_value(json!({})).unwrap(); + for (host, accepted) in [ + ( + "Update package.json scripts so that dev is 'next dev -p 60302'.", + true, + ), + ( + "Update package.json scripts so that dev is 'next dev -p 3000'.", + false, + ), + ( + "Update package.json scripts so that start is 'next start -p 60302'.", + true, + ), + ] { + let addition = ProfileAddition::new(before.steps[0].clone(), &before.steps[0], host.into()); + let sources = CapturedSources::new( + &c, + FormationScope::capture(&before, Some(&addition)), + "test_saved_original_boundary", + ); + let catalog = PackageScripts::capture(&c, &before, sources, &empty); + assert_eq!( + catalog.obligations.len(), + usize::from(accepted), + "{host}: {}", + catalog.guidance() + ); + let mut after = before.clone(); + after.steps[0].verify = vec![check()]; + after.steps[0].instruction.push_str(&format!("\n{host}")); + let (projected, records) = + super::super::verifier_formation::project(&before, &after, Some(&catalog)); + assert_eq!(records.len(), usize::from(accepted)); + assert_eq!(admission::preserve(&before, &projected).is_ok(), accepted); + } + // Duplicate declarations of one key are checked as a set, not last-wins. + for (extra, accepted) in [("next dev -p 60302", true), ("next dev -p 3000", false)] { + let p = single_owner(&format!( + "Update package.json scripts so that dev is 'next dev -p 60302' and dev is '{extra}'." + )); + let catalog = PackageScripts::capture( + &c, + &p, + CapturedSources::new(&c, FormationScope::capture(&p, None), "test"), + &empty, + ); + assert_eq!(catalog.obligations.len(), usize::from(accepted)); + } +} + +#[path = "issue484_runtime_tests.rs"] +mod runtime; + +#[path = "issue484_owner_tests.rs"] +mod owner_scope; + +#[path = "issue484_capture_tests.rs"] +mod capture; diff --git a/src/planner/recovery_step_plan_binding/package_owner_scope.rs b/src/planner/recovery_step_plan_binding/package_owner_scope.rs new file mode 100644 index 00000000..20905da0 --- /dev/null +++ b/src/planner/recovery_step_plan_binding/package_owner_scope.rs @@ -0,0 +1,146 @@ +//! The saved package owner plus full host guidance exceeds one instruction's +//! existing limit. Admit an explicit, ordered split without truncating either +//! duty or asking unrelated owners to discharge it. +use super::*; +use crate::minimal_loop::evidence::package_script_check; +use crate::planner::recovery_contract_authority::verifier_obligations as scope; + +pub(super) fn views( + model: &StepPlan, + host: &StepPlan, + original: &StepPlan, + proposed: &StepPlan, +) -> Option<(StepPlan, StepPlan)> { + let [host_duty] = host.steps.as_slice() else { + return None; + }; + let model_duty = model.steps.iter().find(|s| s.id == host_duty.id)?; + if model_duty.expected_paths != ["package.json"] + || host_duty.expected_paths != ["package.json"] + || !host_duty.verify.is_empty() + || [model_duty, host_duty] + .iter() + .any(|s| s.step_kind() != StepKind::Implement || s.expected_result != "pass") + || !model_duty + .verify + .iter() + .any(|c| package_script_check::printed_script(c).is_some()) + { + return None; + } + let owners = writers(proposed)?; + let [(model_index, model_owner), (host_index, host_owner)] = owners.as_slice() else { + return None; + }; + if model_owner.id != model_duty.id + || model_owner.instruction != model_duty.instruction + || host_owner.instruction != host_duty.instruction + || model.steps.iter().any(|s| s.id == host_owner.id) + || host_owner.id.is_empty() + || [*model_owner, *host_owner].iter().any(|s| { + s.step_kind() != StepKind::Implement + || s.expected_result != "pass" + || s.expected_paths != ["package.json"] + || s.verify.iter().any(|c| { + !crate::planner::profiles::nextjs::recovery_authority::is_package_check(c) + }) + }) + { + return None; + } + let original_owner = original.steps.iter().find(|s| s.id == model_duty.id)?; + // Preserve the complete check group, including host-added checks, after + // both executable writers. Views below do not erase this ordering proof. + if !proposed.steps.iter().enumerate().any(|(index, step)| { + index > *host_index + && step.step_kind() == StepKind::Verify + && step.expected_result == "pass" + && original_owner + .verify + .iter() + .all(|c| step.verify.contains(c)) + }) { + return None; + } + let mut model_view = proposed.clone(); + model_view.steps.remove(*host_index); + let mut host_view = proposed.clone(); + host_view.steps.remove(*model_index); + Some((model_view, host_view)) +} + +fn writers(plan: &StepPlan) -> Option> { + if plan + .steps + .iter() + .flat_map(|s| &s.expected_paths) + .any(|p| scope::normalized(p).is_err()) + { + return None; + } + Some( + plan.steps + .iter() + .enumerate() + .filter(|(_, s)| { + s.step_kind() != StepKind::Verify + && s.expected_paths + .iter() + .any(|p| scope::normalized(p).is_ok_and(|p| p == "package.json")) + }) + .collect(), + ) +} + +/// Lint can recognize only this concrete ordered configuration split. Admission +/// separately proves identity against the immutable model/host source snapshots. +/// This never authorizes a general duplicate output or an arbitrary second writer. +pub(crate) fn ordered_split(plan: &StepPlan, path: &str, first: &str, second: &str) -> bool { + let Some(owners) = writers(plan) else { + return false; + }; + let [(model_index, model), (host_index, host)] = owners.as_slice() else { + return false; + }; + if path != "package.json" + || model.id != first + || host.id != second + || first == second + || *model_index >= *host_index + || host.verify.is_empty() + || [*model, *host].iter().any(|s| { + s.step_kind() != StepKind::Implement + || s.expected_result != "pass" + || s.expected_paths != ["package.json"] + || s.verify.iter().any(|c| { + !crate::planner::profiles::nextjs::recovery_authority::is_package_check(c) + }) + }) + { + return false; + } + let runtime = crate::planner::profile::resolve_profile_runtime( + crate::planner::profile_descriptor::NEXTJS_PROFILE_ID, + ); + if runtime.guidance(&host.instruction).as_deref() != Some(host.instruction.as_str()) { + return false; + } + plan.steps.iter().enumerate().any(|(index, step)| { + index > *host_index + && step.step_kind() == StepKind::Verify + && step.expected_result == "pass" + && model + .verify + .iter() + .chain(&host.verify) + .all(|c| step.verify.contains(c)) + && step.verify.iter().any(|c| { + package_script_check::comparison(c).is_some_and(|(script, value)| { + super::package_script_formation::fixed_values(&model.instruction, &script) + .is_ok_and(|values| { + !values.is_empty() && values.iter().all(|v| *v == value) + }) + }) + }) + }) +} diff --git a/src/planner/recovery_step_plan_binding/package_script_formation.rs b/src/planner/recovery_step_plan_binding/package_script_formation.rs new file mode 100644 index 00000000..78733bfc --- /dev/null +++ b/src/planner/recovery_step_plan_binding/package_script_formation.rs @@ -0,0 +1,342 @@ +//! Expectations come only from the first saved duties. Later proposals and +//! workspace contents are deliberately absent from expectation acquisition. +use super::{formation_scope::FormationScope, *}; +use crate::minimal_loop::evidence::package_script_check as grammar; +use crate::planner::recovery_contract_authority::verifier_obligations as scope; +use sha2::{Digest, Sha256}; + +pub(super) fn hash(value: &[u8]) -> String { + format!("{:x}", Sha256::digest(value)) +} + +fn object_hash(value: &impl serde::Serialize) -> String { + hash(&serde_json::to_vec(value).expect("serializable obligation")) +} + +#[derive(Clone, serde::Serialize)] +pub(super) struct CapturedSources { + pub(super) plans: FormationScope, + source_file: Option, + acquisition_stage: &'static str, + model_sha256: String, + host_sha256: String, +} + +impl CapturedSources { + pub(super) fn new(config: &Config, plans: FormationScope, stage: &'static str) -> Self { + Self { + model_sha256: object_hash(&plans.model), + host_sha256: object_hash(&plans.host), + plans, + source_file: config + .eval_events_path + .as_ref() + .map(|p| p.display().to_string()), + acquisition_stage: stage, + } + } +} + +#[derive(Clone, PartialEq, Eq, serde::Serialize)] +struct Source { + kind: &'static str, + json_pointer: String, + owner: String, + step_index: usize, + step_sha256: String, + instruction_sha256: String, + fixed_literals: Vec, +} + +#[derive(Clone, PartialEq, Eq, serde::Serialize)] +pub(super) struct Obligation { + pub(super) original_command: String, + original_command_sha256: String, + pub(super) expected_result: String, + target: &'static str, + script: String, + expected_literal: String, + original_check_owner: String, + original_step_index: usize, + sources: Vec, + pub(super) replacement_command: String, + replacement_command_sha256: String, + output: &'static str, + failure_propagation: &'static str, +} + +#[derive(serde::Serialize)] +pub(super) struct PackageScripts { + sources: CapturedSources, + registered_contract: CompletionContract, + registered_contract_sha256: String, + original_verify_commands: Vec, + pub(super) obligations: Vec, + refusals: Vec, +} + +impl PackageScripts { + pub(super) fn preserve_raw_commands(&self, model: &StepPlan) -> anyhow::Result<()> { + for command in model.steps.iter().flat_map(|s| &s.verify) { + let normalized = crate::planner::verify::normalize_planner_verify_command(command) + .unwrap_or_default(); + if self.obligations.iter().any(|o| { + command != &o.replacement_command && normalized.contains(&o.replacement_command) + }) { + return Err(crate::planner::recovery_inspection::verifier_obligations::failure( + crate::planner::recovery_inspection::verifier_obligations::FailureClass::ProposalRepairable, + "package-script proposal must preserve the literal comparison, stdout and nonzero failure without shell wrappers", + )); + } + } + Ok(()) + } + pub(super) fn capture( + config: &Config, + original: &StepPlan, + sources: CapturedSources, + contract: &CompletionContract, + ) -> Self { + let mut captured = Self { + sources, + registered_contract: contract.clone(), + registered_contract_sha256: object_hash(contract), + original_verify_commands: original + .steps + .iter() + .flat_map(|s| s.verify.clone()) + .collect(), + obligations: Vec::new(), + refusals: Vec::new(), + }; + for (index, step) in original.steps.iter().enumerate() { + for command in &step.verify { + let Some(script) = grammar::printed_script(command) else { + continue; + }; + match captured.resolve(config, &script) { + Ok((expected, sources)) if step.expected_result == "pass" => { + let replacement = grammar::formed_command(&script, &expected); + captured.obligations.push(Obligation { + original_command: command.clone(), + original_command_sha256: hash(command.as_bytes()), + expected_result: step.expected_result.clone(), target: "./package.json", + script, expected_literal: expected, original_check_owner: step.id.clone(), + original_step_index: index, sources, + replacement_command_sha256: hash(replacement.as_bytes()), + replacement_command: replacement, + output: "print actual string plus newline after strict comparison", + failure_propagation: "uncaught require/property/assertion failure exits nonzero", + }); + } + Ok(_) => captured + .refusals + .push(format!("{command}: original expected result must be pass")), + Err(reason) => captured.refusals.push(format!("{command}: {reason}")), + } + } + } + captured + } + + fn resolve(&self, config: &Config, script: &str) -> Result<(String, Vec), String> { + let mut values = std::collections::BTreeSet::new(); + let mut sources = Vec::new(); + let mut port_constraints = Vec::new(); + let runtime = crate::planner::profile::resolve_profile_runtime(&config.profile); + for (kind, plan) in [ + ("model", &self.sources.plans.model), + ("host", &self.sources.plans.host), + ] { + for (index, step) in plan.steps.iter().enumerate() { + if !step + .expected_paths + .iter() + .any(|p| scope::normalized(p).is_ok_and(|p| p == "package.json")) + { + continue; + } + if step.id.is_empty() + || step.step_kind() != StepKind::Implement + || step.expected_result != "pass" + { + return Err( + "package-script expected value requires an executable Implement/pass owner" + .into(), + ); + } + let known_host = kind == "host" + && runtime.guidance(&plan.goal).as_deref() == Some(step.instruction.as_str()); + let literals = if known_host + && crate::planner::profile::is_nextjs_profile(&config.profile) + { + if matches!(script, "dev" | "start") { + port_constraints.push( + crate::planner::profiles::nextjs::requested_or_default_port(&plan.goal), + ); + } + if script == "build" { + vec!["next build".into()] + } else { + Vec::new() + } + } else { + fixed_values(&step.instruction, script).map_err(str::to_string)? + }; + values.extend(literals.iter().cloned()); + sources.push(Source { + kind, + json_pointer: format!( + "/package_script_formation/sources/plans/{kind}/steps/{index}" + ), + owner: step.id.clone(), + step_index: index, + step_sha256: object_hash(step), + instruction_sha256: hash(step.instruction.as_bytes()), + fixed_literals: literals, + }); + } + } + if values.len() != 1 { + return Err(if values.is_empty() { + "no fixed package-script literal in saved original obligations" + } else { + "conflicting package-script literals in saved model/host obligations" + } + .into()); + } + let expected = values.into_iter().next().expect("one literal"); + for command in self + .registered_contract + .verify_commands + .iter() + .chain(&self.original_verify_commands) + { + if let Some((registered_script, registered_value)) = grammar::comparison(command) + && registered_script == script + && registered_value != expected + { + return Err("saved literal conflicts with registered completion contract".into()); + } + let known_package_check = + crate::planner::profiles::nextjs::recovery_authority::is_package_check(command); + if known_package_check + && script == "build" + && !command.contains("--port=") + && expected != "next build" + { + return Err( + "saved literal conflicts with registered build-script constraint".into(), + ); + } + if command.contains("package.json") + && command.contains("scripts") + && !known_package_check + && grammar::comparison(command).is_none() + && grammar::printed_script(command).is_none() + { + return Err( + "unsupported registered package-script constraint; cannot prove consistency" + .into(), + ); + } + if crate::planner::profiles::nextjs::recovery_authority::is_package_check(command) + && let Some((_, suffix)) = command.split_once("--port=") + && let Some((port, _)) = suffix.split_once('\'') + && let Ok(port) = port.parse::() + { + port_constraints.push(port); + } + } + if matches!(script, "dev" | "start") + && port_constraints.iter().any(|port| { + expected != format!("next {script} -p {port}") + && expected != format!("next {script} --port {port}") + && expected != format!("next {script} --port={port}") + && expected != format!("next {script} -p{port}") + }) + { + return Err( + "saved literal conflicts with original host/registered port constraint".into(), + ); + } + Ok((expected, sources)) + } + + pub(super) fn guidance(&self) -> String { + let mut lines = self.refusals.clone(); + lines.extend(self.obligations.iter().map(|o| format!( + "Saved package-script obligation {} (owner {}, expected {:?}): replace only with {}", + o.original_command, o.original_check_owner, o.expected_literal, o.replacement_command + ))); + if !self.obligations.is_empty() { + lines.push("If complete model and host package instructions exceed one step, explicitly split into two ordered Implement/pass package.json writers: unchanged original model owner first, full original host instruction on a new unique owner second. Keep every original package check together in a Verify/pass after both writers. Do not truncate either instruction, add a third writer or omit host checks.".into()); + } + lines.join("\n") + } +} + +/// The observed declaration is a deliberately closed template, not a natural +/// language interpreter. Extra mentions of the same script are ambiguous. +pub(super) fn fixed_values(instruction: &str, script: &str) -> Result, &'static str> { + let Some(mut rest) = instruction.strip_prefix("Update package.json scripts so that ") else { + return if instruction == "Maintain package.json." { + Ok(Vec::new()) + } else { + Err("ambiguous or unsupported saved package-script declaration") + }; + }; + let mut values = Vec::new(); + loop { + let (name, tail) = rest + .split_once(" is '") + .ok_or("unsupported saved package-script declaration")?; + let (value, tail) = tail + .split_once('\'') + .ok_or("unterminated saved package-script literal")?; + if !grammar::identifier(name) || !grammar::literal(value) { + return Err("unsupported saved package-script name/literal"); + } + if name == script { + values.push(value.into()); + } + if let Some(tail) = tail.strip_prefix(" and ") { + rest = tail; + continue; + } + let tail = tail + .strip_prefix('.') + .ok_or("ambiguous saved package-script alternatives")?; + values.extend(trailing_values(tail, script)?); + return Ok(values); + } +} + +fn trailing_values(mut tail: &str, script: &str) -> Result, &'static str> { + let mut values = Vec::new(); + while let Some(declaration) = tail.strip_prefix(" Keep scripts.") { + let (name, rest) = declaration + .split_once(" as '") + .ok_or("unsupported saved script constraint")?; + let (value, rest) = rest + .split_once('\'') + .ok_or("unterminated saved script constraint")?; + if !grammar::identifier(name) || !grammar::literal(value) { + return Err("unsupported saved script constraint"); + } + if name == script { + values.push(value.into()); + } + tail = rest + .strip_prefix('.') + .ok_or("ambiguous saved script constraint")?; + } + // The saved non-script duties are an explicit finite suffix, not an + // invitation to interpret arbitrary natural-language overrides such as + // "Or use any port" (which need not mention the script key again). + const SAVED_CONFIGURATION_DUTIES: &str = " Ensure next, react, react-dom dependencies are present and tailwindcss, postcss, autoprefixer are present. Do not modify tsconfig.json unless it has moduleResolution=node10 or target=ES5—in that case set moduleResolution to 'bundler' and target to 'ES2017'. Keep postcss.config.js plugins including both tailwindcss and autoprefixer. Keep tailwind.config.ts coherent with content paths covering src/app/**/*.tsx."; + if !tail.is_empty() && tail != SAVED_CONFIGURATION_DUTIES { + return Err("ambiguous or unsupported additional saved package requirements"); + } + Ok(values) +} diff --git a/src/planner/recovery_step_plan_binding/verifier_formation.rs b/src/planner/recovery_step_plan_binding/verifier_formation.rs index c1cd6894..9e3ca9dd 100644 --- a/src/planner/recovery_step_plan_binding/verifier_formation.rs +++ b/src/planner/recovery_step_plan_binding/verifier_formation.rs @@ -7,13 +7,20 @@ use crate::planner::recovery_inspection::verifier_obligations::{FailureClass, fa #[derive(Clone, PartialEq, Eq, serde::Serialize)] pub(super) struct Replacement { original_command: String, - import_target: String, + #[serde(skip_serializing_if = "Option::is_none")] + import_target: Option, expected_result: String, replacement_command: String, reason: &'static str, + #[serde(skip_serializing_if = "Option::is_none")] + package_script: Option, } -pub(super) fn project(original: &StepPlan, proposed: &StepPlan) -> (StepPlan, Vec) { +pub(super) fn project( + original: &StepPlan, + proposed: &StepPlan, + package_scripts: Option<&super::package_script_formation::PackageScripts>, +) -> (StepPlan, Vec) { let mut projected = proposed.clone(); let mut records = Vec::new(); for step in &original.steps { @@ -38,7 +45,7 @@ pub(super) fn project(original: &StepPlan, proposed: &StepPlan) -> (StepPlan, Ve } records.push(Replacement { original_command: command.clone(), - import_target: target.clone(), + import_target: Some(target.clone()), expected_result: step.expected_result.clone(), replacement_command: replacement.clone(), reason: if structural { @@ -46,12 +53,36 @@ pub(super) fn project(original: &StepPlan, proposed: &StepPlan) -> (StepPlan, Ve } else { "identical_import_then_direct_target_assertion_without_catch" }, + package_script: None, }); *replacement = command.clone(); } } } } + if let Some(package_scripts) = package_scripts { + for obligation in &package_scripts.obligations { + for candidate in &mut projected.steps { + if candidate.expected_result != obligation.expected_result { + continue; + } + for replacement in &mut candidate.verify { + // Canonical whole-command equality preserves output, failure + // propagation, target and the literal acquired before retry. + if *replacement != obligation.replacement_command { + continue; + } + records.push(Replacement { + original_command: obligation.original_command.clone(), import_target: None, + expected_result: obligation.expected_result.clone(), replacement_command: replacement.clone(), + reason: "saved_package_script_literal_strict_comparison_then_original_output", + package_script: Some(obligation.clone()), + }); + *replacement = obligation.original_command.clone(); + } + } + } + } (projected, records) } @@ -82,7 +113,7 @@ pub(super) fn require_formed(config: &Config, commands: &[String]) -> anyhow::Re return Err(failure( FailureClass::ProposalRepairable, format!( - "preclosure weak inline verification requires formation: {reason}\nPreserve each original import target and pass result. Replace a pure import only with a target-specific check in this closed shape: node -e \"import('./original-path.js').then(actual=>{{require('node:assert/strict').deepStrictEqual(actual.exportName,42)}})\". Supply the actual required export/value, or actual.exportName(...[literal JSON arguments]) and a literal JSON expected result. Alternatively explicitly propose the sorted runtime export-name set using deepStrictEqual(Object.keys(actual).sort(),[literal JSON names]); an explicitly empty set can describe a type-only module. That alternative verifies loadability and runtime export boundary only, not interface fields or business behavior. Do not infer its expectation from a filename. Preserve all original instructions, outputs and owner order. No unrelated assertion, added check beside the weak original, catch or success override discharges it. Unsupported checks must be reproposed with preserved scope or stop within the current budget." + "preclosure weak inline verification requires formation: {reason}\nPackage-script value displays require a fixed literal in saved original model/host obligations; a value absent from the command alone is not grounds for refusal. Use only the supplied saved-literal replacement, including strict comparison and original stdout. Dynamic properties, wrappers, ambiguous/conflicting or absent original literals are unsupported. Never infer expectations from artifacts or a later proposal.\nPreserve each original import target and pass result. Replace a pure import only with a target-specific check in this closed shape: node -e \"import('./original-path.js').then(actual=>{{require('node:assert/strict').deepStrictEqual(actual.exportName,42)}})\". Supply the actual required export/value, or actual.exportName(...[literal JSON arguments]) and a literal JSON expected result. Alternatively explicitly propose the sorted runtime export-name set using deepStrictEqual(Object.keys(actual).sort(),[literal JSON names]); an explicitly empty set can describe a type-only module. That alternative verifies loadability and runtime export boundary only, not interface fields or business behavior. Do not infer its expectation from a filename. Preserve all original instructions, outputs and owner order. No unrelated assertion, added check beside the weak original, catch or success override discharges it. Unsupported checks must be reproposed with preserved scope or stop within the current budget." ), )); } diff --git a/src/planner/setup_step_policy/implementation_duties.rs b/src/planner/setup_step_policy/implementation_duties.rs index 2461a389..5e5375e3 100644 --- a/src/planner/setup_step_policy/implementation_duties.rs +++ b/src/planner/setup_step_policy/implementation_duties.rs @@ -3,7 +3,8 @@ use super::{ domain_profile, is_nextjs_profile, merge_unique_paths, profile_owns_declared_paths, profile_setup_checks, references_template_owned_artifacts, }; -use crate::planner::step_plan::PlanStep; +use crate::minimal_loop::evidence::package_script_check; +use crate::planner::step_plan::{PlanStep, StepKind}; use std::path::Path; // Broad profile implementation instructions cannot be discharged by the @@ -11,7 +12,11 @@ use std::path::Path; // admission authority comes from the recorded augmentation, never this text. pub(super) fn carries_profile_implementation(profile: &str, step: &PlanStep) -> bool { is_nextjs_profile(profile) - && (step.instruction.contains("\n\nProfile contract:") + && (step + .instruction + .starts_with("Update package.json scripts so that ") + || has_package_observation(step) + || step.instruction.contains("\n\nProfile contract:") || domain_profile(profile) .guidance("") .is_some_and(|guidance| { @@ -21,6 +26,14 @@ pub(super) fn carries_profile_implementation(profile: &str, step: &PlanStep) -> })) } +fn has_package_observation(step: &PlanStep) -> bool { + step.verify.iter().any(|c| { + package_script_check::comparison(c).is_some() + || step.step_kind() == StepKind::Verify + && package_script_check::printed_script(c).is_some() + }) +} + pub(super) fn retain_implementation_checks( root: &Path, profile: &str, @@ -31,6 +44,11 @@ pub(super) fn retain_implementation_checks( if !carries_profile_implementation(profile, step) { return false; } + // A declared observation already has its own target and output boundary. + // Preserve it verbatim instead of replacing it with the profile subset. + if step.step_kind() == StepKind::Verify && has_package_observation(step) { + return true; + } if references_template_owned_artifacts(profile, step) && profile_owns_declared_paths(root, profile, step) && let Some(checks) = profile_setup_checks(root, profile, goal, step, phase_id) diff --git a/src/planner/verify.rs b/src/planner/verify.rs index 3fdfd7c4..da740ac9 100644 --- a/src/planner/verify.rs +++ b/src/planner/verify.rs @@ -1,3 +1,6 @@ +mod setup_command_policy; +use setup_command_policy::is_setup_or_dev_server_verify_command; + use std::path::Path; use crate::minimal_loop::build_verifier::{ @@ -1749,8 +1752,7 @@ pub fn diagnose_verify_command(command: &str) -> VerifyCommandDiagnosis { }; return verify_command_violation(repair.normalized, violation, Some(repair.reason)); } - let lower = normalized.to_ascii_lowercase(); - if is_setup_or_dev_server_verify_command(&lower) { + if is_setup_or_dev_server_verify_command(&normalized) { return verify_command_violation( normalized, VerifyCommandViolationKind::SetupOrDevServer, @@ -2680,26 +2682,6 @@ pub fn package_json_port_script_check_command(port: &str) -> String { ) } -fn is_setup_or_dev_server_verify_command(lower: &str) -> bool { - if lower.starts_with("node -p ") || lower.starts_with("node --print ") { - return false; - } - dependency_install_verify_segment(lower).is_some() - || lower.contains("cargo install") - || lower.contains("npm run dev") - || lower.contains("pnpm dev") - || lower.contains("yarn dev") - || lower.contains("next dev") - || lower.contains("vite --host") - || lower.contains("vite --port") - || (lower.contains("curl ") && is_localhost_reference(lower)) - || (lower.contains("wget ") && is_localhost_reference(lower)) - || lower.contains("python -m http.server") - || lower.contains("python3 -m http.server") - || lower.contains("server start") - || lower.contains("serve ") -} - pub fn dependency_install_verify_segment(command: &str) -> Option { let tokens = shell_words_with_spans(command)?; let values = tokens diff --git a/src/planner/verify/setup_command_policy.rs b/src/planner/verify/setup_command_policy.rs new file mode 100644 index 00000000..4a6b9cd3 --- /dev/null +++ b/src/planner/verify/setup_command_policy.rs @@ -0,0 +1,27 @@ +//! Distinguish a closed package-value comparison from actual setup/server work. +use super::{dependency_install_verify_segment, is_localhost_reference}; + +pub(super) fn is_setup_or_dev_server_verify_command(command: &str) -> bool { + if crate::minimal_loop::evidence::package_script_check::comparison(command).is_some() { + return false; + } + let lower = command.to_ascii_lowercase(); + let lower = lower.as_str(); + if lower.starts_with("node -p ") || lower.starts_with("node --print ") { + return false; + } + dependency_install_verify_segment(lower).is_some() + || lower.contains("cargo install") + || lower.contains("npm run dev") + || lower.contains("pnpm dev") + || lower.contains("yarn dev") + || lower.contains("next dev") + || lower.contains("vite --host") + || lower.contains("vite --port") + || (lower.contains("curl ") && is_localhost_reference(lower)) + || (lower.contains("wget ") && is_localhost_reference(lower)) + || lower.contains("python -m http.server") + || lower.contains("python3 -m http.server") + || lower.contains("server start") + || lower.contains("serve ") +} diff --git a/tests/corpus/apps/issue484-package-script-formation/README.md b/tests/corpus/apps/issue484-package-script-formation/README.md new file mode 100644 index 00000000..9b7216e7 --- /dev/null +++ b/tests/corpus/apps/issue484-package-script-formation/README.md @@ -0,0 +1,37 @@ +# Issue #484 package-script formation + +`original-plan.json`, `saved-scope.json`, and `saved-obligation-sources.json` +retain the saved first R0 proposal and admission observation. The source manifest +identifies the immutable parent evidence and exact owner/literal pointer. +`formed-plan.json` proposes the strict comparison with preserved stdout and +explicitly splits the complete model and host package duties to fit the existing +step instruction limit. The full original check group follows both writers. No live model or saved worker path is +needed. `package.json` is a synthetic runtime control, not expectation authority. + +Unit tests replay this input through runner/admission and execute Node on +matching/missing/mismatched package variants. Refusals include changes to target, +literal, owner, other-key duties, ordering, stdout and failure propagation. +Unsupported-source controls reject dynamic properties, wrappers and unsafe +literal characters before execution. The corpus acceptance control deliberately +fails implementation evidence although the structural comparison succeeds. + +`owner-refusals.json` exercises loss or ambiguity in the bounded two-writer +split, including path aliases and the invariant that host source verify is empty. + +Read provenance in `dev-reports/issue-484/formation-evidence.json` as follows: +`initial_rejection` is the `recovery_verifier_plan_admission` event for the first +retained proposal (`planner_attempt` identifies it); `validated` is its later +`preclosure_verifier_replacements_validated` event. The snapshot's `source_file` +identifies the emitted event stream, while `Source.json_pointer` is relative to +the event object itself. Follow it into `package_script_formation.sources.plans` +and check the owner/step/instruction hashes. The focused test resolves every +pointer and recomputes the hashes. The historical input stream and exact position +are separately pinned by `source-manifest.json` (file SHA-256 and JSON pointer). + +Acquisition is the augmentation stage of the same proposal first retained by +Admission, not necessarily the first proposal ever generated. Pending snapshots +are replaced after an earlier Ready-then-lint-retry; after retention, later +proposals cannot replace them. Missing events/sources are not reconstructed from +app artifacts or later proposals. Lint's ordered-split check is only a structural +check; the positive fixture also passes actual runner Admission against saved +sources before lint. Parent CI/UAT and final product acceptance remain separate. diff --git a/tests/corpus/apps/issue484-package-script-formation/expectations.toml b/tests/corpus/apps/issue484-package-script-formation/expectations.toml new file mode 100644 index 00000000..2b8dfe3b --- /dev/null +++ b/tests/corpus/apps/issue484-package-script-formation/expectations.toml @@ -0,0 +1,21 @@ +case_id = "issue484-package-script-formation" +source = "Saved R0 owner literal and real admission/Node controls; configuration is not implementation evidence" +required_paths = ["package.json"] +verify_commands = ["node -e \"const actual=require('./package.json').scripts.dev;require('node:assert/strict').strictEqual(actual,\\\"next dev -p 60302\\\");console.log(actual)\""] +required_capabilities = [] +required_evidence = ["implementation_artifact"] +required_obligations = [] +deferred_verify_requirements = [] +evidence_hint_tokens = [] +acceptance_passed = false +primary_reason = "missing_required_evidence:implementation_artifact" + +[weak_evidence] +absent = ["node_smoke_without_assertion"] + +[compile] +expect = "not_checked" + +[fixture_contains] +source-manifest.json = ["248d0ab5", "configure-port-and-scripts", "next dev -p 60302"] +refusals.json = ["wrong_target", "output_suppression", "drop_other_key_duty"] diff --git a/tests/corpus/apps/issue484-package-script-formation/formed-plan.json b/tests/corpus/apps/issue484-package-script-formation/formed-plan.json new file mode 100644 index 00000000..c4ecc68e --- /dev/null +++ b/tests/corpus/apps/issue484-package-script-formation/formed-plan.json @@ -0,0 +1,110 @@ +{ + "goal": "Original ultra goal: 小さな代理店向けのプロジェクト管理アプリを構築してください。ユーザーはプロジェクトの作成、期限付きタスクの追加、チームメンバーへの割り当てができ、進捗状況(未着手・進行中・完了)でタスクを絞り込めるようにします。60302ポートで起動可能なNext.jsアプリとして開発してください。\nProfile: nextjs\nStyle: default\nIntent: create\nPhase id: core-implementation\nPhase task: Implement the core task-specific behavior for: 小さな代理店向けのプロジェクト管理アプリを構築してください。ユーザーはプロジェクトの作成、期限付きタスクの追加、チームメンバーへの割り当てができ、進捗状況(未着手・進行中・完了)でタスクを絞り込めるようにします。60302ポートで起動可能なNext.jsアプリとして開発してください。. Keep one route-bound implementation, implement the requested interface and replace unrelated scaffold content, and keep the implementation in the Next.js route-bound source. For TypeScript apps, put shared domain and request/response types in src/lib/types.ts and import the same types from page and route modules. Use imported helpers and export every function consumed by another module. Decide one JSON response shape per operation (bare array, {items}, or {item}) and make the UI read exactly what the route returns. Check Response.ok before accepting a mutation; return errors as {error, details?} and display them in the UI. Connect each requested UI action through its API to persistence and reload the committed result.\n\nWorkspace snapshot:\n- .next\n- next-env.d.ts\n- node_modules\n- package-lock.json\n- package.json\n- postcss.config.js\n- src\n- tailwind.config.ts\n- tsconfig.json\n\nPrior ultra context:\n- Completed phases:\n - project-setup (2/2)\n\nUnmet final requirements from earlier phases:\n- none\n\nRequested features not yet detected:\n- accepting\n- affordance\n- alone\n- another\n- anvil\n- api\n- … and 112 more\n\nProfile generation rules:\n- Profile nextjs/create: preserve a real Next.js app contract. Include next/react/react-dom dependencies, keep scripts.build as next build, and end with a build verification phase. Put dependency setup before any npm run build verification when node_modules is not already present; setup instructions may install dependencies, but verify must not contain npm install. If dependency setup is not allowed or cannot run, stop with dependency_missing instead of claiming build success. Keep a single route-bound implementation; do not leave capability components unimported. For interactive UI, implement the requested interface, replacing unrelated scaffold content, and preserve data-anvil-* attributes on route-bound UI: data-anvil-action=\"primary\" on the main start/submit/action control, data-anvil-action=\"input\" on the main text entry surface when one exists, and data-anvil-state with a JSON snapshot of meaningful visible state after each render. When the contract includes start_or_restart_flow, every restart affordance (including terminal and active states when present) should carry data-anvil-action=\"restart\"; the initial primary action alone cannot satisfy recovery verification. A restart reachable during active use (hook or keyboard shortcut) allows behavioral verification, while an overlay-only restart may verify as unverified:terminal_state_not_reached (partial). If you use Tailwind utility classes or @tailwind directives, include tailwindcss/postcss/autoprefixer and create tailwind.config.* plus postcss.config.*; postcss.config plugins must include BOTH tailwindcss and autoprefixer. Otherwise use plain CSS and do not write Tailwind utility classes. Keep scripts.dev and scripts.start on the explicit requested port when the goal or plan requests one; otherwise use port 3011 with next dev/start -p 3011 or --port 3011.\n- For TypeScript apps, put shared domain and request/response types in src/lib/types.ts and import the same types from page and route modules. Use imported helpers and export every function consumed by another module. Decide one JSON response shape per operation (bare array, {items}, or {item}) and make the UI read exactly what the route returns. Check Response.ok before accepting a mutation; return errors as {error, details?} and display them in the UI. Connect each requested UI action through its API to persistence and reload the committed result.\n- For server-side file persistence, initialize only a missing file (ENOENT); distinguish valid empty data from missing data. Parse errors, directory replacement and other read or write I/O failures must return 5xx without overwriting any existing or related data. Serialize the complete read-modify-write transaction across all writers (including multiple processes), or use generation checks with conflict handling; write to a temporary file and atomically rename it. Atomic rename alone does not prevent lost updates.\n- Validate the complete proposed state before committing: aggregate quantities by entity key before checking availability, scope totals to the requested period, define rejection/cascade/reassignment for referenced parent deletion, and match strict/inclusive comparisons to the goal wording. Reject non-finite or invalid quantities and amounts at the API boundary.\n\nProfile runtime contract:\n- Preserve the workspace as a real Next.js app.\n- Keep next/react/react-dom dependencies in package.json.\n- Keep scripts.build as next build; do not replace it with echo/skip/no-op commands.\n- If npm run build cannot run because dependencies are not installed, report dependency_missing or use an explicit setup step; do not fake success.\n- Keep scripts.dev and scripts.start on the explicitly requested port 60302: next dev/start -p 60302 or --port 60302.\n- If using Tailwind utility classes or @tailwind directives, keep the Tailwind toolchain complete: tailwindcss/postcss/autoprefixer dependencies, tailwind.config.*, and postcss.config plugins with BOTH tailwindcss and autoprefixer. Otherwise use plain CSS.\n- Keep TypeScript and app router configuration coherent.\n- Keep a single route-bound implementation; do not leave capability components unimported.\n- For interactive UI, expose data-anvil-action=\"primary\" on the main start/submit/action control, data-anvil-action=\"input\" on the main text entry surface when one exists, and data-anvil-state with a JSON snapshot of meaningful visible state after each render. When the contract includes start_or_restart_flow, every restart affordance (including terminal and active states when present) should carry data-anvil-action=\"restart\"; the initial primary action alone cannot satisfy recovery verification. A restart reachable during active use (hook or keyboard shortcut) allows behavioral verification, while an overlay-only restart may verify as unverified:terminal_state_not_reached (partial).\n- Do not treat scaffold-only, package-only, or build-only output as complete.\nRoute-bound implementation constraint:\n- Keep a single route-bound implementation; do not leave capability components unimported.\n\nPre-provisioned scaffold note:\n- Required scaffold files are authored before phase 1 when absent; verify or extend the scaffold rather than re-planning file creation.\nDeterministic verification preference:\n- npm run build\n\n\nRequired final artifacts:\n- package.json\n- tsconfig.json\n- postcss.config.js\n- tailwind.config.ts\n- src/app/layout.tsx\n- src/app/page.tsx\n- src/app/globals.css\n- src/app/global.d.ts\n\nRequired final capabilities:\n- stateful_interaction\n- user_input_or_action\n- visible_state_change\n- persistence\n\nRequired final evidence:\n- nextjs_route_evidence\n- build_command_or_dependency_missing_boundary\n- implementation_artifact\n- visible_interactive_surface_evidence\n- user_input_handler_evidence\n- stateful_update_evidence\n- interactive_ui_source_evidence\n- non_static_screen_evidence\n- persistence_evidence", + "steps": [ + { + "expected_paths": [], + "expected_result": "pass", + "id": "inspect-workspace", + "instruction": "Read existing package.json, tsconfig.json, postcss.config.js, tailwind.config.ts, src/app/layout.tsx, src/app/page.tsx, src/app/globals.css, and src/app/global.d.ts to understand the pre-provisioned scaffold structure, dependency versions, and Tailwind/PostCSS configuration before implementing.", + "kind": "inspect", + "verify": [] + }, + { + "expected_paths": [ + "src/lib/types.ts", + "src/lib/store.ts" + ], + "expected_result": "pass", + "id": "create-types-and-store", + "instruction": "Create src/lib/types.ts exporting domain types: Project (id, name, createdAt), Task (id, projectId, title, deadline, status: 'not-started'|'in-progress'|'completed', assignedMemberId, createdAt), Member (id, name), StatusFilter ('all'|'not-started'|'in-progress'|'completed'), and request/response types (CreateProjectRequest, CreateTaskRequest, CreateMemberRequest, ProjectResponse, TaskResponse, MemberResponse, ErrorResponse with error and optional details). Create src/lib/store.ts exporting functions loadProjects, saveProjects, loadTasks, saveTasks, loadMembers, saveMembers that read/write JSON files under src/data/ (projects.json, tasks.json, members.json). Each load function initialises only on ENOENT (missing file) with an empty array; parse errors or I/O failures throw. Each save function serialises the full array to a temp file then atomically renames it. Use a module-level mutex or generation counter to prevent concurrent lost updates.", + "kind": "implement", + "verify": [ + "test -f src/lib/types.ts && test -f src/lib/store.ts" + ] + }, + { + "expected_paths": [ + "src/app/api/projects/route.ts", + "src/app/api/tasks/route.ts", + "src/app/api/members/route.ts" + ], + "expected_result": "pass", + "id": "create-api-routes", + "instruction": "Create three API route modules importing types from src/lib/types.ts and store functions from src/lib/store.ts. 1) src/app/api/projects/route.ts: GET returns {items: Project[]}; POST validates name is non-empty string, creates project with crypto.randomUUID, persists via saveProjects, returns {item: Project} with 201; invalid input returns 400 {error, details}. 2) src/app/api/tasks/route.ts: GET accepts query params projectId and status (StatusFilter), filters tasks accordingly, returns {items: Task[]}; POST validates title, deadline (valid ISO date string), projectId (must exist), assignedMemberId (must exist or empty), status (default 'not-started'), creates task, persists, returns {item: Task} with 201. 3) src/app/api/members/route.ts: GET returns {items: Member[]}; POST validates name, creates member, persists, returns {item: Member} with 201. All error responses use {error, details?} shape. Every function consumed across modules must be exported.", + "kind": "implement", + "verify": [ + "test -f src/app/api/projects/route.ts && test -f src/app/api/tasks/route.ts && test -f src/app/api/members/route.ts" + ] + }, + { + "expected_paths": [ + "src/app/page.tsx", + "src/app/layout.tsx" + ], + "expected_result": "pass", + "id": "create-page-and-layout", + "instruction": "Rewrite src/app/page.tsx as a client component (\"use client\") that is the single route-bound implementation. It must: (a) import domain types from src/lib/types.ts; (b) render a project creation form with a text input (data-anvil-action=\"input\" on the main name field) and a submit button (data-anvil-action=\"primary\"); (c) render a task creation form with fields for title, deadline (date input), project select, member select, and status; (d) render a status filter dropdown (all, not-started, in-progress, completed) that re-fetches tasks from /api/tasks?status=…; (e) display tasks grouped or listed showing title, deadline, assigned member, and status badge; (f) call fetch to /api/projects, /api/tasks, /api/members on mount and after each mutation; (g) check Response.ok before accepting a mutation response; on !ok parse {error, details?} and display the error message in the UI; (h) set data-anvil-state on the root div to a JSON string snapshot including currentProjects, currentTasks, currentMembers, currentFilter, and any errorMessage after each render; (i) connect create-project, create-task, and filter actions through their API endpoints to persistence and reload committed results. Rewrite src/app/layout.tsx to keep the existing Tailwind metadata and globals.css import, update the html lang to 'ja', and set the title to 'プロジェクト管理'. Ensure no capability component is left unimported; all interactive logic lives in page.tsx.", + "kind": "implement", + "verify": [ + "test -f src/app/page.tsx && test -f src/app/layout.tsx" + ] + }, + { + "expected_paths": [ + "package.json" + ], + "expected_result": "pass", + "id": "configure-port-and-scripts", + "instruction": "Update package.json scripts so that dev is 'next dev -p 60302' and start is 'next start -p 60302'. Keep scripts.build as 'next build'. Ensure next, react, react-dom dependencies are present and tailwindcss, postcss, autoprefixer are present. Do not modify tsconfig.json unless it has moduleResolution=node10 or target=ES5—in that case set moduleResolution to 'bundler' and target to 'ES2017'. Keep postcss.config.js plugins including both tailwindcss and autoprefixer. Keep tailwind.config.ts coherent with content paths covering src/app/**/*.tsx.", + "kind": "implement", + "verify": [] + }, + { + "expected_paths": [ + "package.json" + ], + "expected_result": "pass", + "id": "preserve-profile-package-duty", + "instruction": "For the nextjs profile, create a runnable Next.js app, not only package metadata. Keep the project in the workspace root unless a project subdirectory already exists. Preserve the language and styling toolchains declared by an existing project. A JavaScript project using plain CSS requires package.json, an App Router page and layout, and the imported stylesheet; do not add TypeScript, @types, Tailwind, PostCSS, or Autoprefixer only to satisfy a template. A TypeScript project additionally requires a coherent tsconfig and type dependencies. If those mode-appropriate files are absent, write the coherent App Router scaffold before further inspection. package.json must include compatible next, react, and react-dom dependencies plus scripts.build = `next build`. If Tailwind is used, package.json must include tailwindcss/postcss/autoprefixer and postcss.config plugins must include BOTH tailwindcss and autoprefixer. For TypeScript/TSX apps, create tsconfig.json before treating the app as complete. Keep a single route-bound implementation; do not leave capability components unimported. Do not use deprecated moduleResolution=node10 or target=ES5; prefer moduleResolution=bundler and target=ES2017 or newer. For interactive UI, expose task-agnostic observability hooks: data-anvil-action=\"primary\" on the main start/submit/action control, data-anvil-action=\"input\" on the main text entry surface when one exists, and data-anvil-state containing JSON for meaningful visible state after each render. The data-anvil-state snapshot must include at least one dimension that immediately responds to input, such as the current selection or edited value. When the contract includes start_or_restart_flow, every restart affordance (including terminal and active states when present) should carry data-anvil-action=\"restart\"; the initial primary action alone cannot satisfy recovery verification. A restart reachable during active use (hook or keyboard shortcut) allows behavioral verification, while an overlay-only restart may verify as unverified:terminal_state_not_reached (partial). The dev/start scripts must run on the explicitly requested port 60302: `next dev -p 60302` and `next start -p 60302` or equivalent `--port 60302` forms.", + "kind": "implement", + "verify": [] + }, + { + "id": "verify-package-scripts", + "kind": "verify", + "expected_result": "pass", + "instruction": "Run every original package check after both package owners and preserve stdout and nonzero failures.", + "expected_paths": [], + "verify": [ + "node -e \"const actual=require('./package.json').scripts.dev;require('node:assert/strict').strictEqual(actual,\\\"next dev -p 60302\\\");console.log(actual)\"", + "node -p \"['dev'].every(function(k){return String(Object(require('./package.json').scripts)[k]).split(' ').some(function(t,i,a){return t=='next' ? a.slice(i+1).find(function(x){return x})==k : false}) ? String(Object(require('./package.json').scripts)[k]).split(' ').some(function(t,i,a){return t=='--port=60302' ? true : t=='-p' ? a.slice(i+1).find(function(x){return x})=='60302' : t=='-p60302' ? true : t=='--port' ? a.slice(i+1).find(function(x){return x})=='60302' : false}) : false}) ? true : process.exit(1)\"", + "node -p \"['start'].every(function(k){return Object(require('./package.json').scripts)[k] ? String(Object(require('./package.json').scripts)[k]).split(' ').some(function(t,i,a){return t=='next' ? a.slice(i+1).find(function(x){return x})==k : false}) ? String(Object(require('./package.json').scripts)[k]).split(' ').some(function(t,i,a){return t=='--port=60302' ? true : t=='-p' ? a.slice(i+1).find(function(x){return x})=='60302' : t=='-p60302' ? true : t=='--port' ? a.slice(i+1).find(function(x){return x})=='60302' : false}) : false : true}) ? true : process.exit(1)\"", + "node -p \"String(require('./package.json').scripts.build)=='next build' ? true : process.exit(1)\"" + ] + }, + { + "expected_paths": [ + "src/app/page.tsx", + "src/app/layout.tsx", + "src/app/api/projects/route.ts", + "src/app/api/tasks/route.ts", + "src/app/api/members/route.ts", + "src/lib/types.ts", + "src/lib/store.ts", + "package.json", + "tsconfig.json", + "postcss.config.js", + "tailwind.config.ts", + "src/app/globals.css", + "src/app/global.d.ts" + ], + "expected_result": "pass", + "id": "build-verification", + "instruction": "Run npm run build to verify the full Next.js TypeScript application compiles, all API routes type-check, the client component in src/app/page.tsx imports types from src/lib/types.ts and helpers from src/lib/store.ts without errors, and no unimported capability modules remain. The build must succeed with exit code 0.", + "kind": "verify", + "verify": [ + "npm run build" + ] + } + ] +} diff --git a/tests/corpus/apps/issue484-package-script-formation/original-plan.json b/tests/corpus/apps/issue484-package-script-formation/original-plan.json new file mode 100644 index 00000000..88949374 --- /dev/null +++ b/tests/corpus/apps/issue484-package-script-formation/original-plan.json @@ -0,0 +1,89 @@ +{ + "goal": "Original ultra goal: 小さな代理店向けのプロジェクト管理アプリを構築してください。ユーザーはプロジェクトの作成、期限付きタスクの追加、チームメンバーへの割り当てができ、進捗状況(未着手・進行中・完了)でタスクを絞り込めるようにします。60302ポートで起動可能なNext.jsアプリとして開発してください。\nProfile: nextjs\nStyle: default\nIntent: create\nPhase id: core-implementation\nPhase task: Implement the core task-specific behavior for: 小さな代理店向けのプロジェクト管理アプリを構築してください。ユーザーはプロジェクトの作成、期限付きタスクの追加、チームメンバーへの割り当てができ、進捗状況(未着手・進行中・完了)でタスクを絞り込めるようにします。60302ポートで起動可能なNext.jsアプリとして開発してください。. Keep one route-bound implementation, implement the requested interface and replace unrelated scaffold content, and keep the implementation in the Next.js route-bound source. For TypeScript apps, put shared domain and request/response types in src/lib/types.ts and import the same types from page and route modules. Use imported helpers and export every function consumed by another module. Decide one JSON response shape per operation (bare array, {items}, or {item}) and make the UI read exactly what the route returns. Check Response.ok before accepting a mutation; return errors as {error, details?} and display them in the UI. Connect each requested UI action through its API to persistence and reload the committed result.\n\nWorkspace snapshot:\n- .next\n- next-env.d.ts\n- node_modules\n- package-lock.json\n- package.json\n- postcss.config.js\n- src\n- tailwind.config.ts\n- tsconfig.json\n\nPrior ultra context:\n- Completed phases:\n - project-setup (2/2)\n\nUnmet final requirements from earlier phases:\n- none\n\nRequested features not yet detected:\n- accepting\n- affordance\n- alone\n- another\n- anvil\n- api\n- … and 112 more\n\nProfile generation rules:\n- Profile nextjs/create: preserve a real Next.js app contract. Include next/react/react-dom dependencies, keep scripts.build as next build, and end with a build verification phase. Put dependency setup before any npm run build verification when node_modules is not already present; setup instructions may install dependencies, but verify must not contain npm install. If dependency setup is not allowed or cannot run, stop with dependency_missing instead of claiming build success. Keep a single route-bound implementation; do not leave capability components unimported. For interactive UI, implement the requested interface, replacing unrelated scaffold content, and preserve data-anvil-* attributes on route-bound UI: data-anvil-action=\"primary\" on the main start/submit/action control, data-anvil-action=\"input\" on the main text entry surface when one exists, and data-anvil-state with a JSON snapshot of meaningful visible state after each render. When the contract includes start_or_restart_flow, every restart affordance (including terminal and active states when present) should carry data-anvil-action=\"restart\"; the initial primary action alone cannot satisfy recovery verification. A restart reachable during active use (hook or keyboard shortcut) allows behavioral verification, while an overlay-only restart may verify as unverified:terminal_state_not_reached (partial). If you use Tailwind utility classes or @tailwind directives, include tailwindcss/postcss/autoprefixer and create tailwind.config.* plus postcss.config.*; postcss.config plugins must include BOTH tailwindcss and autoprefixer. Otherwise use plain CSS and do not write Tailwind utility classes. Keep scripts.dev and scripts.start on the explicit requested port when the goal or plan requests one; otherwise use port 3011 with next dev/start -p 3011 or --port 3011.\n- For TypeScript apps, put shared domain and request/response types in src/lib/types.ts and import the same types from page and route modules. Use imported helpers and export every function consumed by another module. Decide one JSON response shape per operation (bare array, {items}, or {item}) and make the UI read exactly what the route returns. Check Response.ok before accepting a mutation; return errors as {error, details?} and display them in the UI. Connect each requested UI action through its API to persistence and reload the committed result.\n- For server-side file persistence, initialize only a missing file (ENOENT); distinguish valid empty data from missing data. Parse errors, directory replacement and other read or write I/O failures must return 5xx without overwriting any existing or related data. Serialize the complete read-modify-write transaction across all writers (including multiple processes), or use generation checks with conflict handling; write to a temporary file and atomically rename it. Atomic rename alone does not prevent lost updates.\n- Validate the complete proposed state before committing: aggregate quantities by entity key before checking availability, scope totals to the requested period, define rejection/cascade/reassignment for referenced parent deletion, and match strict/inclusive comparisons to the goal wording. Reject non-finite or invalid quantities and amounts at the API boundary.\n\nProfile runtime contract:\n- Preserve the workspace as a real Next.js app.\n- Keep next/react/react-dom dependencies in package.json.\n- Keep scripts.build as next build; do not replace it with echo/skip/no-op commands.\n- If npm run build cannot run because dependencies are not installed, report dependency_missing or use an explicit setup step; do not fake success.\n- Keep scripts.dev and scripts.start on the explicitly requested port 60302: next dev/start -p 60302 or --port 60302.\n- If using Tailwind utility classes or @tailwind directives, keep the Tailwind toolchain complete: tailwindcss/postcss/autoprefixer dependencies, tailwind.config.*, and postcss.config plugins with BOTH tailwindcss and autoprefixer. Otherwise use plain CSS.\n- Keep TypeScript and app router configuration coherent.\n- Keep a single route-bound implementation; do not leave capability components unimported.\n- For interactive UI, expose data-anvil-action=\"primary\" on the main start/submit/action control, data-anvil-action=\"input\" on the main text entry surface when one exists, and data-anvil-state with a JSON snapshot of meaningful visible state after each render. When the contract includes start_or_restart_flow, every restart affordance (including terminal and active states when present) should carry data-anvil-action=\"restart\"; the initial primary action alone cannot satisfy recovery verification. A restart reachable during active use (hook or keyboard shortcut) allows behavioral verification, while an overlay-only restart may verify as unverified:terminal_state_not_reached (partial).\n- Do not treat scaffold-only, package-only, or build-only output as complete.\nRoute-bound implementation constraint:\n- Keep a single route-bound implementation; do not leave capability components unimported.\n\nPre-provisioned scaffold note:\n- Required scaffold files are authored before phase 1 when absent; verify or extend the scaffold rather than re-planning file creation.\nDeterministic verification preference:\n- npm run build\n\n\nRequired final artifacts:\n- package.json\n- tsconfig.json\n- postcss.config.js\n- tailwind.config.ts\n- src/app/layout.tsx\n- src/app/page.tsx\n- src/app/globals.css\n- src/app/global.d.ts\n\nRequired final capabilities:\n- stateful_interaction\n- user_input_or_action\n- visible_state_change\n- persistence\n\nRequired final evidence:\n- nextjs_route_evidence\n- build_command_or_dependency_missing_boundary\n- implementation_artifact\n- visible_interactive_surface_evidence\n- user_input_handler_evidence\n- stateful_update_evidence\n- interactive_ui_source_evidence\n- non_static_screen_evidence\n- persistence_evidence", + "steps": [ + { + "expected_paths": [], + "expected_result": "pass", + "id": "inspect-workspace", + "instruction": "Read existing package.json, tsconfig.json, postcss.config.js, tailwind.config.ts, src/app/layout.tsx, src/app/page.tsx, src/app/globals.css, and src/app/global.d.ts to understand the pre-provisioned scaffold structure, dependency versions, and Tailwind/PostCSS configuration before implementing.", + "kind": "inspect", + "verify": [] + }, + { + "expected_paths": [ + "src/lib/types.ts", + "src/lib/store.ts" + ], + "expected_result": "pass", + "id": "create-types-and-store", + "instruction": "Create src/lib/types.ts exporting domain types: Project (id, name, createdAt), Task (id, projectId, title, deadline, status: 'not-started'|'in-progress'|'completed', assignedMemberId, createdAt), Member (id, name), StatusFilter ('all'|'not-started'|'in-progress'|'completed'), and request/response types (CreateProjectRequest, CreateTaskRequest, CreateMemberRequest, ProjectResponse, TaskResponse, MemberResponse, ErrorResponse with error and optional details). Create src/lib/store.ts exporting functions loadProjects, saveProjects, loadTasks, saveTasks, loadMembers, saveMembers that read/write JSON files under src/data/ (projects.json, tasks.json, members.json). Each load function initialises only on ENOENT (missing file) with an empty array; parse errors or I/O failures throw. Each save function serialises the full array to a temp file then atomically renames it. Use a module-level mutex or generation counter to prevent concurrent lost updates.", + "kind": "implement", + "verify": [ + "test -f src/lib/types.ts && test -f src/lib/store.ts" + ] + }, + { + "expected_paths": [ + "src/app/api/projects/route.ts", + "src/app/api/tasks/route.ts", + "src/app/api/members/route.ts" + ], + "expected_result": "pass", + "id": "create-api-routes", + "instruction": "Create three API route modules importing types from src/lib/types.ts and store functions from src/lib/store.ts. 1) src/app/api/projects/route.ts: GET returns {items: Project[]}; POST validates name is non-empty string, creates project with crypto.randomUUID, persists via saveProjects, returns {item: Project} with 201; invalid input returns 400 {error, details}. 2) src/app/api/tasks/route.ts: GET accepts query params projectId and status (StatusFilter), filters tasks accordingly, returns {items: Task[]}; POST validates title, deadline (valid ISO date string), projectId (must exist), assignedMemberId (must exist or empty), status (default 'not-started'), creates task, persists, returns {item: Task} with 201. 3) src/app/api/members/route.ts: GET returns {items: Member[]}; POST validates name, creates member, persists, returns {item: Member} with 201. All error responses use {error, details?} shape. Every function consumed across modules must be exported.", + "kind": "implement", + "verify": [ + "test -f src/app/api/projects/route.ts && test -f src/app/api/tasks/route.ts && test -f src/app/api/members/route.ts" + ] + }, + { + "expected_paths": [ + "src/app/page.tsx", + "src/app/layout.tsx" + ], + "expected_result": "pass", + "id": "create-page-and-layout", + "instruction": "Rewrite src/app/page.tsx as a client component (\"use client\") that is the single route-bound implementation. It must: (a) import domain types from src/lib/types.ts; (b) render a project creation form with a text input (data-anvil-action=\"input\" on the main name field) and a submit button (data-anvil-action=\"primary\"); (c) render a task creation form with fields for title, deadline (date input), project select, member select, and status; (d) render a status filter dropdown (all, not-started, in-progress, completed) that re-fetches tasks from /api/tasks?status=…; (e) display tasks grouped or listed showing title, deadline, assigned member, and status badge; (f) call fetch to /api/projects, /api/tasks, /api/members on mount and after each mutation; (g) check Response.ok before accepting a mutation response; on !ok parse {error, details?} and display the error message in the UI; (h) set data-anvil-state on the root div to a JSON string snapshot including currentProjects, currentTasks, currentMembers, currentFilter, and any errorMessage after each render; (i) connect create-project, create-task, and filter actions through their API endpoints to persistence and reload committed results. Rewrite src/app/layout.tsx to keep the existing Tailwind metadata and globals.css import, update the html lang to 'ja', and set the title to 'プロジェクト管理'. Ensure no capability component is left unimported; all interactive logic lives in page.tsx.", + "kind": "implement", + "verify": [ + "test -f src/app/page.tsx && test -f src/app/layout.tsx" + ] + }, + { + "expected_paths": [ + "package.json" + ], + "expected_result": "pass", + "id": "configure-port-and-scripts", + "instruction": "Update package.json scripts so that dev is 'next dev -p 60302' and start is 'next start -p 60302'. Keep scripts.build as 'next build'. Ensure next, react, react-dom dependencies are present and tailwindcss, postcss, autoprefixer are present. Do not modify tsconfig.json unless it has moduleResolution=node10 or target=ES5—in that case set moduleResolution to 'bundler' and target to 'ES2017'. Keep postcss.config.js plugins including both tailwindcss and autoprefixer. Keep tailwind.config.ts coherent with content paths covering src/app/**/*.tsx.", + "kind": "implement", + "verify": [ + "node -p \"require('./package.json').scripts.dev\"" + ] + }, + { + "expected_paths": [ + "src/app/page.tsx", + "src/app/layout.tsx", + "src/app/api/projects/route.ts", + "src/app/api/tasks/route.ts", + "src/app/api/members/route.ts", + "src/lib/types.ts", + "src/lib/store.ts", + "package.json", + "tsconfig.json", + "postcss.config.js", + "tailwind.config.ts", + "src/app/globals.css", + "src/app/global.d.ts" + ], + "expected_result": "pass", + "id": "build-verification", + "instruction": "Run npm run build to verify the full Next.js TypeScript application compiles, all API routes type-check, the client component in src/app/page.tsx imports types from src/lib/types.ts and helpers from src/lib/store.ts without errors, and no unimported capability modules remain. The build must succeed with exit code 0.", + "kind": "verify", + "verify": [ + "npm run build" + ] + } + ] +} diff --git a/tests/corpus/apps/issue484-package-script-formation/owner-refusals.json b/tests/corpus/apps/issue484-package-script-formation/owner-refusals.json new file mode 100644 index 00000000..76291460 --- /dev/null +++ b/tests/corpus/apps/issue484-package-script-formation/owner-refusals.json @@ -0,0 +1,20 @@ +[ + "model_text", + "host_text", + "model_result", + "host_result", + "model_scope", + "host_scope", + "delete_model", + "delete_host", + "third_writer", + "alias_third_writer", + "non_normalized_writer", + "same_id", + "host_before_model", + "check_between_writers", + "check_before_writers", + "partial_checks", + "host_source_verify", + "host_source_result" +] diff --git a/tests/corpus/apps/issue484-package-script-formation/package.json b/tests/corpus/apps/issue484-package-script-formation/package.json new file mode 100644 index 00000000..e24138b7 --- /dev/null +++ b/tests/corpus/apps/issue484-package-script-formation/package.json @@ -0,0 +1,9 @@ +{ + "name": "issue484-structural-control", + "private": true, + "scripts": { + "dev": "next dev -p 60302", + "start": "next start -p 60302", + "build": "next build" + } +} diff --git a/tests/corpus/apps/issue484-package-script-formation/refusals.json b/tests/corpus/apps/issue484-package-script-formation/refusals.json new file mode 100644 index 00000000..533d398f --- /dev/null +++ b/tests/corpus/apps/issue484-package-script-formation/refusals.json @@ -0,0 +1,17 @@ +[ + "wrong_target", + "changed_literal", + "changed_script", + "delete_check", + "changed_result", + "unrelated_assertion", + "missing_owner", + "nonexecuting_owner", + "premature_check", + "swallow_failure", + "wrapper", + "output_suppression", + "append_beside_original", + "drop_other_key_duty", + "artifact_derived_literal" +] diff --git a/tests/corpus/apps/issue484-package-script-formation/saved-obligation-sources.json b/tests/corpus/apps/issue484-package-script-formation/saved-obligation-sources.json new file mode 100644 index 00000000..b2b41a1e --- /dev/null +++ b/tests/corpus/apps/issue484-package-script-formation/saved-obligation-sources.json @@ -0,0 +1,102 @@ +{ + "host": { + "goal": "Implement the core task-specific behavior for: 小さな代理店向けのプロジェクト管理アプリを構築してください。", + "steps": [ + { + "expected_paths": [ + "package.json" + ], + "expected_result": "pass", + "id": "configure-port-and-scripts", + "instruction": "For the nextjs profile, create a runnable Next.js app, not only package metadata. Keep the project in the workspace root unless a project subdirectory already exists. Preserve the language and styling toolchains declared by an existing project. A JavaScript project using plain CSS requires package.json, an App Router page and layout, and the imported stylesheet; do not add TypeScript, @types, Tailwind, PostCSS, or Autoprefixer only to satisfy a template. A TypeScript project additionally requires a coherent tsconfig and type dependencies. If those mode-appropriate files are absent, write the coherent App Router scaffold before further inspection. package.json must include compatible next, react, and react-dom dependencies plus scripts.build = `next build`. If Tailwind is used, package.json must include tailwindcss/postcss/autoprefixer and postcss.config plugins must include BOTH tailwindcss and autoprefixer. For TypeScript/TSX apps, create tsconfig.json before treating the app as complete. Keep a single route-bound implementation; do not leave capability components unimported. Do not use deprecated moduleResolution=node10 or target=ES5; prefer moduleResolution=bundler and target=ES2017 or newer. For interactive UI, expose task-agnostic observability hooks: data-anvil-action=\"primary\" on the main start/submit/action control, data-anvil-action=\"input\" on the main text entry surface when one exists, and data-anvil-state containing JSON for meaningful visible state after each render. The data-anvil-state snapshot must include at least one dimension that immediately responds to input, such as the current selection or edited value. When the contract includes start_or_restart_flow, every restart affordance (including terminal and active states when present) should carry data-anvil-action=\"restart\"; the initial primary action alone cannot satisfy recovery verification. A restart reachable during active use (hook or keyboard shortcut) allows behavioral verification, while an overlay-only restart may verify as unverified:terminal_state_not_reached (partial). The dev/start scripts must run on the explicitly requested port 60302: `next dev -p 60302` and `next start -p 60302` or equivalent `--port 60302` forms.", + "kind": "implement", + "verify": [] + } + ] + }, + "model": { + "goal": "Implement the core task-specific behavior for: 小さな代理店向けのプロジェクト管理アプリを構築してください。", + "steps": [ + { + "expected_paths": [ + "package.json" + ], + "expected_result": "pass", + "id": "inspect-workspace", + "instruction": "Verify the profile-owned package_manifest contract by running every declared check and report any exact failure.", + "kind": "verify", + "verify": [ + "node -p \"['dev'].every(function(k){return String(Object(require('./package.json').scripts)[k]).split(' ').some(function(t,i,a){return t=='next' ? a.slice(i+1).find(function(x){return x})==k : false}) ? String(Object(require('./package.json').scripts)[k]).split(' ').some(function(t,i,a){return t=='--port=60302' ? true : t=='-p' ? a.slice(i+1).find(function(x){return x})=='60302' : t=='-p60302' ? true : t=='--port' ? a.slice(i+1).find(function(x){return x})=='60302' : false}) : false}) ? true : process.exit(1)\"", + "node -p \"['start'].every(function(k){return Object(require('./package.json').scripts)[k] ? String(Object(require('./package.json').scripts)[k]).split(' ').some(function(t,i,a){return t=='next' ? a.slice(i+1).find(function(x){return x})==k : false}) ? String(Object(require('./package.json').scripts)[k]).split(' ').some(function(t,i,a){return t=='--port=60302' ? true : t=='-p' ? a.slice(i+1).find(function(x){return x})=='60302' : t=='-p60302' ? true : t=='--port' ? a.slice(i+1).find(function(x){return x})=='60302' : false}) : false : true}) ? true : process.exit(1)\"", + "node -p \"String(require('./package.json').scripts.build)=='next build' ? true : process.exit(1)\"" + ] + }, + { + "expected_paths": [ + "src/lib/types.ts", + "src/lib/store.ts" + ], + "expected_result": "pass", + "id": "create-types-and-store", + "instruction": "Create src/lib/types.ts exporting domain types: Project (id, name, createdAt), Task (id, projectId, title, deadline, status: 'not-started'|'in-progress'|'completed', assignedMemberId, createdAt), Member (id, name), StatusFilter ('all'|'not-started'|'in-progress'|'completed'), and request/response types (CreateProjectRequest, CreateTaskRequest, CreateMemberRequest, ProjectResponse, TaskResponse, MemberResponse, ErrorResponse with error and optional details). Create src/lib/store.ts exporting functions loadProjects, saveProjects, loadTasks, saveTasks, loadMembers, saveMembers that read/write JSON files under src/data/ (projects.json, tasks.json, members.json). Each load function initialises only on ENOENT (missing file) with an empty array; parse errors or I/O failures throw. Each save function serialises the full array to a temp file then atomically renames it. Use a module-level mutex or generation counter to prevent concurrent lost updates.", + "kind": "implement", + "verify": [ + "test -f src/lib/types.ts", + "test -f src/lib/store.ts" + ] + }, + { + "expected_paths": [ + "src/app/api/projects/route.ts", + "src/app/api/tasks/route.ts", + "src/app/api/members/route.ts" + ], + "expected_result": "pass", + "id": "create-api-routes", + "instruction": "Create three API route modules importing types from src/lib/types.ts and store functions from src/lib/store.ts. 1) src/app/api/projects/route.ts: GET returns {items: Project[]}; POST validates name is non-empty string, creates project with crypto.randomUUID, persists via saveProjects, returns {item: Project} with 201; invalid input returns 400 {error, details}. 2) src/app/api/tasks/route.ts: GET accepts query params projectId and status (StatusFilter), filters tasks accordingly, returns {items: Task[]}; POST validates title, deadline (valid ISO date string), projectId (must exist), assignedMemberId (must exist or empty), status (default 'not-started'), creates task, persists, returns {item: Task} with 201. 3) src/app/api/members/route.ts: GET returns {items: Member[]}; POST validates name, creates member, persists, returns {item: Member} with 201. All error responses use {error, details?} shape. Every function consumed across modules must be exported.", + "kind": "implement", + "verify": [ + "test -f src/app/api/projects/route.ts", + "test -f src/app/api/tasks/route.ts", + "test -f src/app/api/members/route.ts" + ] + }, + { + "expected_paths": [ + "src/app/page.tsx", + "src/app/layout.tsx" + ], + "expected_result": "pass", + "id": "create-page-and-layout", + "instruction": "Rewrite src/app/page.tsx as a client component (\"use client\") that is the single route-bound implementation. It must: (a) import domain types from src/lib/types.ts; (b) render a project creation form with a text input (data-anvil-action=\"input\" on the main name field) and a submit button (data-anvil-action=\"primary\"); (c) render a task creation form with fields for title, deadline (date input), project select, member select, and status; (d) render a status filter dropdown (all, not-started, in-progress, completed) that re-fetches tasks from /api/tasks?status=…; (e) display tasks grouped or listed showing title, deadline, assigned member, and status badge; (f) call fetch to /api/projects, /api/tasks, /api/members on mount and after each mutation; (g) check Response.ok before accepting a mutation response; on !ok parse {error, details?} and display the error message in the UI; (h) set data-anvil-state on the root div to a JSON string snapshot including currentProjects, currentTasks, currentMembers, currentFilter, and any errorMessage after each render; (i) connect create-project, create-task, and filter actions through their API endpoints to persistence and reload committed results. Rewrite src/app/layout.tsx to keep the existing Tailwind metadata and globals.css import, update the html lang to 'ja', and set the title to 'プロジェクト管理'. Ensure no capability component is left unimported; all interactive logic lives in page.tsx.", + "kind": "implement", + "verify": [ + "test -f src/app/page.tsx", + "test -f src/app/layout.tsx" + ] + }, + { + "expected_paths": [ + "package.json" + ], + "expected_result": "pass", + "id": "configure-port-and-scripts", + "instruction": "Update package.json scripts so that dev is 'next dev -p 60302' and start is 'next start -p 60302'. Keep scripts.build as 'next build'. Ensure next, react, react-dom dependencies are present and tailwindcss, postcss, autoprefixer are present. Do not modify tsconfig.json unless it has moduleResolution=node10 or target=ES5—in that case set moduleResolution to 'bundler' and target to 'ES2017'. Keep postcss.config.js plugins including both tailwindcss and autoprefixer. Keep tailwind.config.ts coherent with content paths covering src/app/**/*.tsx.", + "kind": "implement", + "verify": [ + "node -p \"require('./package.json').scripts.dev\"" + ] + }, + { + "expected_paths": [], + "expected_result": "pass", + "id": "build-verification", + "instruction": "Run npm run build to verify the full Next.js TypeScript application compiles, all API routes type-check, the client component in src/app/page.tsx imports types from src/lib/types.ts and helpers from src/lib/store.ts without errors, and no unimported capability modules remain. The build must succeed with exit code 0.", + "kind": "verify", + "verify": [ + "npm run build" + ] + } + ] + } +} diff --git a/tests/corpus/apps/issue484-package-script-formation/saved-scope.json b/tests/corpus/apps/issue484-package-script-formation/saved-scope.json new file mode 100644 index 00000000..eddca6a6 --- /dev/null +++ b/tests/corpus/apps/issue484-package-script-formation/saved-scope.json @@ -0,0 +1,88 @@ +{ + "goal": "Implement the core task-specific behavior for: 小さな代理店向けのプロジェクト管理アプリを構築してください。", + "steps": [ + { + "expected_paths": [ + "package.json" + ], + "expected_result": "pass", + "id": "inspect-workspace", + "instruction": "Verify the profile-owned package_manifest contract by running every declared check and report any exact failure.", + "kind": "verify", + "verify": [ + "node -p \"['dev'].every(function(k){return String(Object(require('./package.json').scripts)[k]).split(' ').some(function(t,i,a){return t=='next' ? a.slice(i+1).find(function(x){return x})==k : false}) ? String(Object(require('./package.json').scripts)[k]).split(' ').some(function(t,i,a){return t=='--port=60302' ? true : t=='-p' ? a.slice(i+1).find(function(x){return x})=='60302' : t=='-p60302' ? true : t=='--port' ? a.slice(i+1).find(function(x){return x})=='60302' : false}) : false}) ? true : process.exit(1)\"", + "node -p \"['start'].every(function(k){return Object(require('./package.json').scripts)[k] ? String(Object(require('./package.json').scripts)[k]).split(' ').some(function(t,i,a){return t=='next' ? a.slice(i+1).find(function(x){return x})==k : false}) ? String(Object(require('./package.json').scripts)[k]).split(' ').some(function(t,i,a){return t=='--port=60302' ? true : t=='-p' ? a.slice(i+1).find(function(x){return x})=='60302' : t=='-p60302' ? true : t=='--port' ? a.slice(i+1).find(function(x){return x})=='60302' : false}) : false : true}) ? true : process.exit(1)\"", + "node -p \"String(require('./package.json').scripts.build)=='next build' ? true : process.exit(1)\"" + ] + }, + { + "expected_paths": [ + "src/lib/types.ts", + "src/lib/store.ts" + ], + "expected_result": "pass", + "id": "create-types-and-store", + "instruction": "Create src/lib/types.ts exporting domain types: Project (id, name, createdAt), Task (id, projectId, title, deadline, status: 'not-started'|'in-progress'|'completed', assignedMemberId, createdAt), Member (id, name), StatusFilter ('all'|'not-started'|'in-progress'|'completed'), and request/response types (CreateProjectRequest, CreateTaskRequest, CreateMemberRequest, ProjectResponse, TaskResponse, MemberResponse, ErrorResponse with error and optional details). Create src/lib/store.ts exporting functions loadProjects, saveProjects, loadTasks, saveTasks, loadMembers, saveMembers that read/write JSON files under src/data/ (projects.json, tasks.json, members.json). Each load function initialises only on ENOENT (missing file) with an empty array; parse errors or I/O failures throw. Each save function serialises the full array to a temp file then atomically renames it. Use a module-level mutex or generation counter to prevent concurrent lost updates.", + "kind": "implement", + "verify": [ + "test -f src/lib/types.ts", + "test -f src/lib/store.ts" + ] + }, + { + "expected_paths": [ + "src/app/api/projects/route.ts", + "src/app/api/tasks/route.ts", + "src/app/api/members/route.ts" + ], + "expected_result": "pass", + "id": "create-api-routes", + "instruction": "Create three API route modules importing types from src/lib/types.ts and store functions from src/lib/store.ts. 1) src/app/api/projects/route.ts: GET returns {items: Project[]}; POST validates name is non-empty string, creates project with crypto.randomUUID, persists via saveProjects, returns {item: Project} with 201; invalid input returns 400 {error, details}. 2) src/app/api/tasks/route.ts: GET accepts query params projectId and status (StatusFilter), filters tasks accordingly, returns {items: Task[]}; POST validates title, deadline (valid ISO date string), projectId (must exist), assignedMemberId (must exist or empty), status (default 'not-started'), creates task, persists, returns {item: Task} with 201. 3) src/app/api/members/route.ts: GET returns {items: Member[]}; POST validates name, creates member, persists, returns {item: Member} with 201. All error responses use {error, details?} shape. Every function consumed across modules must be exported.", + "kind": "implement", + "verify": [ + "test -f src/app/api/projects/route.ts", + "test -f src/app/api/tasks/route.ts", + "test -f src/app/api/members/route.ts" + ] + }, + { + "expected_paths": [ + "src/app/page.tsx", + "src/app/layout.tsx" + ], + "expected_result": "pass", + "id": "create-page-and-layout", + "instruction": "Rewrite src/app/page.tsx as a client component (\"use client\") that is the single route-bound implementation. It must: (a) import domain types from src/lib/types.ts; (b) render a project creation form with a text input (data-anvil-action=\"input\" on the main name field) and a submit button (data-anvil-action=\"primary\"); (c) render a task creation form with fields for title, deadline (date input), project select, member select, and status; (d) render a status filter dropdown (all, not-started, in-progress, completed) that re-fetches tasks from /api/tasks?status=…; (e) display tasks grouped or listed showing title, deadline, assigned member, and status badge; (f) call fetch to /api/projects, /api/tasks, /api/members on mount and after each mutation; (g) check Response.ok before accepting a mutation response; on !ok parse {error, details?} and display the error message in the UI; (h) set data-anvil-state on the root div to a JSON string snapshot including currentProjects, currentTasks, currentMembers, currentFilter, and any errorMessage after each render; (i) connect create-project, create-task, and filter actions through their API endpoints to persistence and reload committed results. Rewrite src/app/layout.tsx to keep the existing Tailwind metadata and globals.css import, update the html lang to 'ja', and set the title to 'プロジェクト管理'. Ensure no capability component is left unimported; all interactive logic lives in page.tsx.", + "kind": "implement", + "verify": [ + "test -f src/app/page.tsx", + "test -f src/app/layout.tsx" + ] + }, + { + "expected_paths": [ + "package.json" + ], + "expected_result": "pass", + "id": "configure-port-and-scripts", + "instruction": "Update package.json scripts so that dev is 'next dev -p 60302' and start is 'next start -p 60302'. Keep scripts.build as 'next build'. Ensure next, react, react-dom dependencies are present and tailwindcss, postcss, autoprefixer are present. Do not modify tsconfig.json unless it has moduleResolution=node10 or target=ES5—in that case set moduleResolution to 'bundler' and target to 'ES2017'. Keep postcss.config.js plugins including both tailwindcss and autoprefixer. Keep tailwind.config.ts coherent with content paths covering src/app/**/*.tsx.\n\nProfile contract:\nFor the nextjs profile, create a runnable Next.js app, not only package metadata. Keep the project in the workspace root unless a project subdirectory already exists. Preserve the language and styling toolchains declared by an existing project. A JavaScript project using plain CSS requires package.json, an App Router page and layout, and the imported stylesheet; do not add TypeScript, @types, Tailwind, PostCSS, or Autoprefixer only to satisfy a template. A TypeScript project additionally requires a coherent tsconfig and type dependencies. If those mode-appropriate files are absent, write the coherent App Router scaffold before further inspection. package.json must include compatible next, react, and react-dom dependencies plus scripts.build = `next build`. If Tailwind is used, package.json must include tailwindcss/postcss/autoprefixer and postcss.config plugins must include BOTH tailwindcss and autoprefixer. For TypeScript/TSX apps, create tsconfig.json before treating the app as complete. Keep a single route-bound implementation; do not leave capability components unimported. Do not use deprecated moduleResolution=node10 or target=ES5; prefer moduleResolution=bundler and target=ES2017 or newer. For interactive UI, expose task-agnostic observability hooks: data-anvil-action=\"primary\" on the main start/submit/action control, data-anvil-action=\"input\" on the main text entry surface when one exists, and data-anvil-state containing JSON for meaningful visible state after each render. The data-anvil-state snapshot must include at least one dimension that immediately responds to input, such as the current selection or edited value. When the contract includes start_or_restart_flow, every restart affordance (including terminal and active states when present) should carry data-anvil-action=\"restart\"; the initial primary action alone cannot satisfy recovery verification.", + "kind": "implement", + "verify": [ + "node -p \"require('./package.json').scripts.dev\"", + "node -p \"['dev'].every(function(k){return String(Object(require('./package.json').scripts)[k]).split(' ').some(function(t,i,a){return t=='next' ? a.slice(i+1).find(function(x){return x})==k : false}) ? String(Object(require('./package.json').scripts)[k]).split(' ').some(function(t,i,a){return t=='--port=60302' ? true : t=='-p' ? a.slice(i+1).find(function(x){return x})=='60302' : t=='-p60302' ? true : t=='--port' ? a.slice(i+1).find(function(x){return x})=='60302' : false}) : false}) ? true : process.exit(1)\"", + "node -p \"['start'].every(function(k){return Object(require('./package.json').scripts)[k] ? String(Object(require('./package.json').scripts)[k]).split(' ').some(function(t,i,a){return t=='next' ? a.slice(i+1).find(function(x){return x})==k : false}) ? String(Object(require('./package.json').scripts)[k]).split(' ').some(function(t,i,a){return t=='--port=60302' ? true : t=='-p' ? a.slice(i+1).find(function(x){return x})=='60302' : t=='-p60302' ? true : t=='--port' ? a.slice(i+1).find(function(x){return x})=='60302' : false}) : false : true}) ? true : process.exit(1)\"", + "node -p \"String(require('./package.json').scripts.build)=='next build' ? true : process.exit(1)\"" + ] + }, + { + "expected_paths": [], + "expected_result": "pass", + "id": "build-verification", + "instruction": "Run npm run build to verify the full Next.js TypeScript application compiles, all API routes type-check, the client component in src/app/page.tsx imports types from src/lib/types.ts and helpers from src/lib/store.ts without errors, and no unimported capability modules remain. The build must succeed with exit code 0.", + "kind": "verify", + "verify": [ + "npm run build" + ] + } + ] +} diff --git a/tests/corpus/apps/issue484-package-script-formation/source-manifest.json b/tests/corpus/apps/issue484-package-script-formation/source-manifest.json new file mode 100644 index 00000000..9951eea9 --- /dev/null +++ b/tests/corpus/apps/issue484-package-script-formation/source-manifest.json @@ -0,0 +1,16 @@ +{ + "source": "workspace/management/runs/20260914-integrated-r0-evaluation-478-480-01/reports/preclosure-admission-observations-01.json", + "source_sha256": "248d0ab5a8958e623aa11d3f857fedc45ed6576c545dc4387b2f779da383bd29", + "json_pointer": "/events/4/event/original_obligation_sources/model/steps/4", + "owner": "configure-port-and-scripts", + "expected_literal": "next dev -p 60302", + "capture": "preclosure attempt 1; live-prefix observation, not terminal evidence", + "derived_fixture": "formed-plan.json replaces the weak command and explicitly splits the complete saved model and host package duties to stay within the unchanged instruction limit; all package checks follow both owners.", + "fixture_sha256": { + "original-plan.json": "fbdaa12997a87c4eb78ca2029d7d5eda3f5d2acf7e9cc6df010930db96302dee", + "saved-scope.json": "cf1f328b394b1f9df6b11372180fab81cccf3fd21292c6acf19e847a86f56af8", + "saved-obligation-sources.json": "278c86e3c8a172b493236bb06ae71610eadf9d147b180ed85aebdb9dab8f076e", + "package.json": "ad2c5affb307d8da85b8fad196d83e89ae3fd4e76133d5b41b903e89613b087c", + "formed-plan.json": "279f565fd9a7a0c8962b2907e02e1fdf7d47d2fbd38bd772e0c12d6fdd27320d" + } +} diff --git a/tests/corpus/apps/issue484-package-script-formation/unsupported-sources.json b/tests/corpus/apps/issue484-package-script-formation/unsupported-sources.json new file mode 100644 index 00000000..e13cf272 --- /dev/null +++ b/tests/corpus/apps/issue484-package-script-formation/unsupported-sources.json @@ -0,0 +1,26 @@ +{ + "commands": [ + "node -p \"require('./package.json').scripts[key]\"", + "node -p \"require('./package.json').scripts['dev']\"", + "node -p \"String(require('./package.json').scripts.dev)\"", + "node -p \"require('./other.json').scripts.dev\"", + "node --print=\"require('./package.json').scripts.dev\"" + ], + "unsafe_literals": [ + "next dev 'quoted'", + "next dev \"quoted\"", + "next dev \\path", + "next dev\nnext start", + "next dev `id`", + "next dev $HOME", + "next dev $(id)", + "next dev; true", + "next dev && true", + "next dev | cat", + "next dev > output", + "next dev あ", + " next dev", + "next dev ", + "next dev" + ] +}