diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cd63858..cc98359 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,6 +5,10 @@ env: on: pull_request: + paths: + - "src/**" + - "crates/**" + - "test/**" jobs: validate: diff --git a/README.md b/README.md index 8163915..1edc2c1 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,7 @@ Inspect detector behavior without count output: ```bash word-counter inspect "こんにちは、世界!これはテストです。" word-counter inspect --detector wasm --view engine "This sentence should clearly be detected as English for the wasm detector path." +word-counter inspect --detector wasm --view engine --content-gate strict "Readers understand this behavior." word-counter inspect --detector regex -f json "こんにちは、世界!これはテストです。" word-counter inspect --detector regex -f json --pretty "こんにちは、世界!これはテストです。" word-counter inspect --detector wasm --content-gate off "mode: debug\ntee: true\npath: logs\nUse this for testing." @@ -144,6 +145,11 @@ Detector mode notes: - Technical-noise-heavy Latin windows stay conservative and may remain `und-Latn` even when the detector produces a wrong-but-confident language guess. - inspect/debug disclosure uses `contentGate` as the canonical gate field. - legacy debug/evidence payloads still emit `qualityGate` as a compatibility alias derived from `contentGate.passed`. +- `inspect --view engine` stays raw: + - it shows the detector sample plus raw/normalized/remapped Whatlang output + - it does not apply `eligibility` or `contentGate` policy decisions + - if engine view uses an explicit or effective non-default content-gate mode, the CLI emits a cyan info note and points to `--view pipeline` +- `inspect --view pipeline` is the inspect surface for `eligibility`, `contentGate`, acceptance, and fallback reasoning. - for practical verification, use `inspect` to compare direct mode outcomes across `default`, `strict`, `loose`, and `off`; use `--debug --detector-evidence` when you specifically need counting-flow event details or legacy `qualityGate` compatibility - `word-counter inspect` supports: - positional text input diff --git a/bun.lock b/bun.lock index 5a4e370..f029619 100644 --- a/bun.lock +++ b/bun.lock @@ -1,9 +1,9 @@ { "lockfileVersion": 1, - "configVersion": 0, + "configVersion": 1, "workspaces": { "": { - "name": "word-counter", + "name": "@dev-pi2pie/word-counter", "dependencies": { "commander": "^14.0.3", "yaml": "^2.8.3", @@ -22,9 +22,6 @@ }, }, }, - "overrides": { - "picomatch": "4.0.4", - }, "packages": { "@babel/generator": ["@babel/generator@8.0.0-rc.3", "", { "dependencies": { "@babel/parser": "^8.0.0-rc.3", "@babel/types": "^8.0.0-rc.3", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "@types/jsesc": "^2.5.0", "jsesc": "^3.0.2" } }, "sha512-em37/13/nR320G4jab/nIIHZgc2Wz2y/D39lxnTyxB4/D/omPQncl/lSdlnJY1OhQcRGugTSIF2l/69o31C9dA=="], @@ -36,11 +33,11 @@ "@babel/types": ["@babel/types@8.0.0-rc.3", "", { "dependencies": { "@babel/helper-string-parser": "^8.0.0-rc.3", "@babel/helper-validator-identifier": "^8.0.0-rc.3" } }, "sha512-mOm5ZrYmphGfqVWoH5YYMTITb3cDXsFgmvFlvkvWDMsR9X8RFnt7a0Wb6yNIdoFsiMO9WjYLq+U/FMtqIYAF8Q=="], - "@emnapi/core": ["@emnapi/core@1.7.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg=="], + "@emnapi/core": ["@emnapi/core@1.9.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.0", "tslib": "^2.4.0" } }, "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA=="], - "@emnapi/runtime": ["@emnapi/runtime@1.7.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA=="], + "@emnapi/runtime": ["@emnapi/runtime@1.9.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA=="], - "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], + "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg=="], "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], @@ -50,7 +47,7 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], - "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" } }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="], + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.2", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw=="], "@oxc-project/types": ["@oxc-project/types@0.122.0", "", {}, "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA=="], @@ -247,13 +244,5 @@ "unrun": ["unrun@0.2.34", "", { "dependencies": { "rolldown": "1.0.0-rc.12" }, "peerDependencies": { "synckit": "^0.11.11" }, "optionalPeers": ["synckit"], "bin": { "unrun": "dist/cli.mjs" } }, "sha512-LyaghRBR++r7svhDK6tnDz2XaYHWdneBOA0jbS8wnRsHerI9MFljX4fIiTgbbNbEVzZ0C9P1OjWLLe1OqoaaEw=="], "yaml": ["yaml@2.8.3", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg=="], - - "ast-kit/@babel/parser": ["@babel/parser@8.0.0-rc.1", "", { "dependencies": { "@babel/types": "^8.0.0-rc.1" }, "bin": "./bin/babel-parser.js" }, "sha512-6HyyU5l1yK/7h9Ki52i5h6mDAx4qJdiLQO4FdCyJNoB/gy3T3GGJdhQzzbZgvgZCugYBvwtQiWRt94QKedHnkA=="], - - "ast-kit/@babel/parser/@babel/types": ["@babel/types@8.0.0-rc.1", "", { "dependencies": { "@babel/helper-string-parser": "^8.0.0-rc.1", "@babel/helper-validator-identifier": "^8.0.0-rc.1" } }, "sha512-ubmJ6TShyaD69VE9DQrlXcdkvJbmwWPB8qYj0H2kaJi29O7vJT9ajSdBd2W8CG34pwL9pYA74fi7RHC1qbLoVQ=="], - - "ast-kit/@babel/parser/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@8.0.0-rc.2", "", {}, "sha512-noLx87RwlBEMrTzncWd/FvTxoJ9+ycHNg0n8yyYydIoDsLZuxknKgWRJUqcrVkNrJ74uGyhWQzQaS3q8xfGAhQ=="], - - "ast-kit/@babel/parser/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@8.0.0-rc.1", "", {}, "sha512-I4YnARytXC2RzkLNVnf5qFNFMzp679qZpmtw/V3Jt2uGnWiIxyJtaukjG7R8pSx8nG2NamICpGfljQsogj+FbQ=="], } } diff --git a/docs/plans/jobs/2026-03-31-inspect-engine-content-gate-info-note-implementation.md b/docs/plans/jobs/2026-03-31-inspect-engine-content-gate-info-note-implementation.md new file mode 100644 index 0000000..a4cee85 --- /dev/null +++ b/docs/plans/jobs/2026-03-31-inspect-engine-content-gate-info-note-implementation.md @@ -0,0 +1,36 @@ +--- +title: "inspect engine content gate info note implementation" +created-date: 2026-03-31 +status: completed +agent: Codex +--- + +## Goal + +Implement the settled `inspect --view engine` info-note behavior so raw engine inspection stays unchanged while users are explicitly redirected to `--view pipeline` when `contentGate` policy expectations would otherwise be misleading. + +## Completed Work + +- Updated `src/cli/inspect/run.ts` to emit a cyan `Info:` note to stderr when: + - `inspect` is running with `--view engine` + - the effective detector is `wasm` + - `--content-gate ...` was explicitly passed, including `default`, or config/env resolves the effective mode to a non-default value +- Kept the note outside the inspect result body so both standard and JSON stdout payloads remain unchanged. +- Ensured the note is emitted once per inspect invocation, including inspect batch runs. +- Updated `src/cli/inspect/help.ts` so `--content-gate` is described as pipeline policy inspection behavior. +- Updated `README.md` and `docs/schemas/detector-inspector-output-contract.md` so engine vs pipeline responsibilities are explicit and the note trigger behavior is documented. +- Marked the implementation plan complete in `docs/plans/plan-2026-03-31-inspect-engine-content-gate-info-note-implementation.md`. + +## Verification + +- `bun test test/command-inspect.test.ts test/command-config-inspect.test.ts` + - 49 tests passed + - 0 tests failed + +## Related Research + +- `docs/researches/research-2026-03-31-inspect-engine-content-gate-info-note.md` + +## Related Plans + +- `docs/plans/plan-2026-03-31-inspect-engine-content-gate-info-note-implementation.md` diff --git a/docs/plans/plan-2026-03-31-inspect-engine-content-gate-info-note-implementation.md b/docs/plans/plan-2026-03-31-inspect-engine-content-gate-info-note-implementation.md new file mode 100644 index 0000000..a3b8934 --- /dev/null +++ b/docs/plans/plan-2026-03-31-inspect-engine-content-gate-info-note-implementation.md @@ -0,0 +1,110 @@ +--- +title: "inspect engine content gate info note implementation" +created-date: 2026-03-31 +modified-date: 2026-03-31 +status: completed +agent: Codex +--- + +## Goal + +Implement the settled `inspect --view engine` UX refinement so `--content-gate ...` keeps its current raw-engine behavior while the CLI emits a cyan informational note that directs users to `--view pipeline` for eligibility and content-gate restriction diagnosis. + +## Context + +- The current `engine` view is intentionally raw and reports the diagnostic sample plus Whatlang output without package-level projection. +- The current `pipeline` view is where `eligibility`, `contentGate`, acceptance, and fallback decisions are evaluated and disclosed. +- The CLI currently accepts `--content-gate ...` together with `--view engine`, but that flag does not change engine output. +- The new research settles that the UX gap should be solved with an informational note rather than with validation rejection or engine-view policy semantics. + +## Scope + +- In scope: + - add an inspect-local informational note for `--view engine` combined with a meaningful content-gate setting + - keep the note styled as cyan/info rather than warning/error output + - define the exact trigger rules for explicit CLI, config, and env-derived content-gate settings + - update inspect help and docs so engine vs pipeline responsibilities are explicit + - add regression coverage for note emission and non-emission cases +- Out of scope: + - changing `engine` view payloads or output fields + - applying `contentGate` policy inside `engine` view + - rejecting `--content-gate` together with `--view engine` + - changing detector thresholds, route policy behavior, or window selection + - broader inspect UX redesign unrelated to this note + +## Decisions Settled for This Plan + +- `--view engine` remains a raw engine/sample/remap surface. +- `--content-gate` remains accepted syntax under `inspect --view engine`. +- The CLI should emit a cyan info note when: + - the user explicitly passes `--content-gate ...`, including `default` + - the effective inspect content-gate mode comes from config or env and resolves to a non-default value +- The CLI should not emit the note for ordinary engine-view runs that simply inherit the default content-gate mode with no explicit or effective override. +- The note should be emitted once per inspect invocation, before single-input or batch rendering begins, so batch runs do not repeat the same message per file. +- The note should explain both facts: + - `--content-gate` does not affect engine-view output + - `--view pipeline` is the correct inspect surface for eligibility and content-gate restriction diagnosis +- The implementation should stay inspect-local rather than introducing a broader shared CLI notice abstraction unless a concrete duplication need appears during coding. + +## Phase Task Items + +### Phase 1 - Effective-Mode Note Emission + +- [x] Identify the inspect command point where config/env application has already resolved the effective detector and content-gate values. +- [x] Add inspect-local logic that emits the cyan info note before execution when: + - `view === "engine"` + - `detector === "wasm"` + - the note trigger rules are satisfied +- [x] Keep the note out of the main inspect result body so standard and JSON result payloads remain unchanged. +- [x] Ensure the note is emitted to stderr rather than being mixed into stdout result output. +- [x] Ensure the note is emitted once per inspect invocation, including batch runs, rather than once per inspected file. + +Validation for this phase: + +- command tests for explicit CLI `--content-gate default|strict|loose|off` with `--view engine` +- command tests proving explicit `--content-gate default` emits the note while inherited default content-gate does not +- command tests for config/env-derived non-default content-gate modes with `--view engine` +- command tests proving ordinary default engine runs do not emit the note +- command tests proving batch engine runs emit the note once per invocation, not once per file + +### Phase 2 - Docs And Help Alignment + +- [x] Update inspect help text to clarify that `--content-gate` affects pipeline policy inspection, not raw engine view. +- [x] Update `README.md` inspect guidance so engine vs pipeline responsibilities are explicit at the user-facing command level. +- [x] Update `docs/schemas/detector-inspector-output-contract.md` to note that engine view ignores content-gate policy and that the CLI may emit an informational note for that combination. +- [x] Keep the wording aligned with the settled research message and cyan/info intent. + +Validation for this phase: + +- docs review against the settled research note +- spot-check command examples so the docs do not imply engine-view gate enforcement + +### Phase 3 - Regression And Contract Audit + +- [x] Confirm that engine-view stdout output remains unchanged for the same input before and after the note feature. +- [x] Confirm that inspect JSON output remains schema-identical and free of note text. +- [x] Confirm that pipeline-view behavior, counting behavior, and detector-subpath inspect library results remain unchanged. + +Validation for this phase: + +- targeted `test/command-inspect.test.ts` coverage for stdout/stderr separation +- targeted regression checks for JSON output stability +- targeted detector inspect tests if implementation touches shared inspect execution code + +## Compatibility Gates + +- [x] `inspect --view engine` continues to return the same detector result data for the same text. +- [x] The new informational note does not alter stdout payloads for either `standard` or `json` output. +- [x] `inspect --view pipeline` remains the only inspect view that exposes `eligibility`, `contentGate`, and fallback reasoning. +- [x] Existing config/env/CLI precedence for effective content-gate mode remains unchanged. + +## Related Research + +- `docs/researches/research-2026-03-31-inspect-engine-content-gate-info-note.md` +- `docs/researches/research-2026-03-25-detector-policy-and-inspector-surface.md` +- `docs/researches/research-2026-03-25-configurable-content-gate-behavior.md` + +## Related Plans + +- `docs/plans/plan-2026-03-25-detector-policy-and-inspect-command.md` +- `docs/plans/plan-2026-03-26-config-content-gate-support.md` diff --git a/docs/researches/research-2026-03-31-inspect-engine-content-gate-info-note.md b/docs/researches/research-2026-03-31-inspect-engine-content-gate-info-note.md new file mode 100644 index 0000000..4f64f57 --- /dev/null +++ b/docs/researches/research-2026-03-31-inspect-engine-content-gate-info-note.md @@ -0,0 +1,67 @@ +--- +title: "inspect engine content gate info note" +created-date: 2026-03-31 +modified-date: 2026-03-31 +status: completed +agent: codex +--- + +## Goal + +Decide how `word-counter inspect --detector wasm --view engine` should behave when `--content-gate ...` is also present, and record the recommended user-facing guidance. + +## Key Findings + +- The current `engine` view is intentionally raw and does not run the package-level policy path. + - In `src/detector/wasm.ts`, `view === "engine"` returns the first ambiguous window through `buildEngineInspectResult(...)`. + - That path reports the diagnostic sample, raw Whatlang result, normalized Whatlang result, and public remap values. + - It does not compute or disclose `eligibility`, `contentGate`, or final acceptance/fallback decisions. +- The current `pipeline` view is where `contentGate` actually matters. + - In `src/detector/wasm-resolution.ts`, the detector route policy evaluates both `eligibility` and `contentGate` before deciding whether the engine result is accepted or falls back. + - This is also the only inspect surface that currently explains why a window was skipped, accepted, or rejected. +- Because the CLI still accepts `--content-gate` together with `--view engine`, users can reasonably infer that the flag should affect engine output. + - In practice, the same text currently produces the same engine-view output across `default`, `strict`, `loose`, and `off`. + - The current behavior is technically consistent with the raw-engine contract, but the UX is misleading because the CLI does not explain the limitation at the point of use. +- Making `contentGate` behavior-changing inside engine view would blur the contract. + - A raw engine view should answer: "what sample was sent to Whatlang, and what did it say?" + - A policy-aware view should answer: "did the package accept that result, and if not, why?" + - Folding policy decisions back into engine view would create ambiguity around window selection, skipped windows, and whether engine output should disappear or be annotated when the window is not policy-eligible. + +## Implications or Recommendations + +- Keep `engine` view raw. + - Preserve the current contract where engine view reports sample construction and Whatlang output without package-level projection. +- Do not reject `--content-gate` when `--view engine` is requested. + - Rejection is defensible, but it is stricter than needed and removes a recoverable learning moment. + - A note is sufficient because the requested command still produces valid raw engine diagnostics. +- Add a cyan informational note when `engine` view is combined with a meaningful content-gate setting. + - Recommended message: + - `Info: \`--content-gate\` does not affect \`inspect --view engine\`; engine view shows raw detector output. Use \`--view pipeline\` to inspect eligibility and content-gate restrictions.` + - Recommended trigger: + - show the note when the user explicitly passed `--content-gate ...`, including `default` + - also show the note when the effective mode comes from config or env and resolves to a non-default value + - Recommended non-trigger: + - do not show the note for ordinary engine-view runs that simply inherit the default mode with no explicit or effective override +- Update inspect-facing docs to make the split explicit. + - `--view engine` should be documented as raw engine/sample/remap output only. + - `--view pipeline` should be documented as the place to inspect `eligibility`, `contentGate`, and fallback reasoning. + - Help text and README examples should steer users toward pipeline view for policy verification. + +## Related Plans + +- [docs/plans/plan-2026-03-25-detector-policy-and-inspect-command.md](docs/plans/plan-2026-03-25-detector-policy-and-inspect-command.md) +- [docs/plans/plan-2026-03-31-inspect-engine-content-gate-info-note-implementation.md](docs/plans/plan-2026-03-31-inspect-engine-content-gate-info-note-implementation.md) + +## Related Research + +- [docs/researches/research-2026-03-25-detector-policy-and-inspector-surface.md](docs/researches/research-2026-03-25-detector-policy-and-inspector-surface.md) +- [docs/researches/research-2026-03-25-configurable-content-gate-behavior.md](docs/researches/research-2026-03-25-configurable-content-gate-behavior.md) + +## References + +[^1]: [docs/researches/research-2026-03-25-detector-policy-and-inspector-surface.md](docs/researches/research-2026-03-25-detector-policy-and-inspector-surface.md) +[^2]: [docs/schemas/detector-inspector-output-contract.md](docs/schemas/detector-inspector-output-contract.md) +[^3]: [src/detector/wasm.ts](src/detector/wasm.ts) +[^4]: [src/detector/wasm-resolution.ts](src/detector/wasm-resolution.ts) +[^5]: [src/cli/inspect/render.ts](src/cli/inspect/render.ts) +[^6]: [src/cli/inspect/help.ts](src/cli/inspect/help.ts) diff --git a/docs/schemas/detector-inspector-output-contract.md b/docs/schemas/detector-inspector-output-contract.md index 685eebf..a997535 100644 --- a/docs/schemas/detector-inspector-output-contract.md +++ b/docs/schemas/detector-inspector-output-contract.md @@ -81,6 +81,8 @@ Shared validation rules: - `off` - `--format raw` is invalid for `inspect` - `--pretty` changes JSON indentation only +- `--content-gate ...` remains accepted with `--view engine`, but engine view does not apply content-gate policy + - the CLI may emit an informational stderr note when engine view uses an explicit or effective non-default content-gate mode - positional inspect input is always treated as text, never auto-resolved as a path - supported inspect section modes are: - `all` @@ -137,6 +139,12 @@ Batch exit rules: `engine` view is WASM-only and reports engine-centered detector output without package-level projection. +Interpretation notes: + +- engine view reports the diagnostic sample plus raw/normalized/remapped Whatlang output only +- engine view does not apply `eligibility`, `contentGate`, acceptance, or fallback policy decisions +- when engine view uses an explicit or effective non-default content-gate mode, the CLI may emit an informational stderr note that points users to `--view pipeline` + Required fields: - `routeTag` diff --git a/package.json b/package.json index 8303487..475003f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@dev-pi2pie/word-counter", - "version": "0.1.6-canary.1", + "version": "0.1.6", "keywords": [ "cli", "intl-segmenter", @@ -71,9 +71,6 @@ "peerDependencies": { "typescript": "^5 || ^6" }, - "overrides": { - "picomatch": "4.0.4" - }, "engines": { "node": ">=22.18.0" } diff --git a/src/cli/inspect/help.ts b/src/cli/inspect/help.ts index 9f10721..cc1d85e 100644 --- a/src/cli/inspect/help.ts +++ b/src/cli/inspect/help.ts @@ -5,7 +5,7 @@ const INSPECT_HELP_LINES = [ "", "Options:", " -d, --detector inspect detector mode (wasm, regex) (default: regex)", - " --content-gate content gate mode (default, strict, loose, off) (default: default)", + " --content-gate content gate mode for pipeline policy inspection (default, strict, loose, off) (default: default)", " --view inspect view (pipeline, engine) (default: pipeline)", " -f, --format inspect output format (standard, json) (default: standard)", " --pretty pretty print inspect JSON output", diff --git a/src/cli/inspect/run.ts b/src/cli/inspect/run.ts index a28370a..8f0d8d3 100644 --- a/src/cli/inspect/run.ts +++ b/src/cli/inspect/run.ts @@ -18,6 +18,29 @@ function emitConfigNotes(notes: string[]): void { } } +function shouldEmitEngineContentGateInfo(validated: { + view: "engine" | "pipeline"; + detector: "wasm" | "regex"; + contentGateMode: "default" | "strict" | "loose" | "off"; + sources: { + contentGate: boolean; + }; +}): boolean { + if (validated.view !== "engine" || validated.detector !== "wasm") { + return false; + } + + return validated.sources.contentGate || validated.contentGateMode !== "default"; +} + +function emitEngineContentGateInfo(): void { + console.error( + pc.cyan( + "Info: `--content-gate` does not affect `inspect --view engine`; engine view shows raw detector output. Use `--view pipeline` to inspect eligibility and content-gate restrictions.", + ), + ); +} + export async function executeInspectCommand({ argv, runtime, @@ -57,6 +80,10 @@ export async function executeInspectCommand({ return; } + if (shouldEmitEngineContentGateInfo(validated)) { + emitEngineContentGateInfo(); + } + try { if (validated.paths.length === 0) { const input = await loadSingleInspectInput( diff --git a/src/cli/program/version-embedded.ts b/src/cli/program/version-embedded.ts index f1ff18f..efbac25 100644 --- a/src/cli/program/version-embedded.ts +++ b/src/cli/program/version-embedded.ts @@ -1,3 +1,3 @@ // This file is generated by scripts/generate-embedded-version.mjs. // Do not edit manually. -export const EMBEDDED_PACKAGE_VERSION = "0.1.6-canary.1"; +export const EMBEDDED_PACKAGE_VERSION = "0.1.6"; diff --git a/test/command-config-inspect.test.ts b/test/command-config-inspect.test.ts index aefcf39..ac153f2 100644 --- a/test/command-config-inspect.test.ts +++ b/test/command-config-inspect.test.ts @@ -66,6 +66,61 @@ describe("CLI inspect config overrides and defaults", () => { expect(parsed.view).toBe("engine"); }); + test("emits the engine-view info note for config-derived non-default content-gate mode", async () => { + if (!hasWasmDetectorRuntime()) { + return; + } + + const cwd = await makeTempFixture("inspect-engine-content-gate-config-note"); + await writeTomlConfig(cwd, ['detector = "wasm"', "", "[contentGate]", 'mode = "strict"']); + + const output = await captureCli( + ["inspect", "--view", "engine", "--format", "json", inspectContentGateText], + { cwd }, + ); + + expect(output.exitCode).toBe(0); + expect( + output.stderr.filter( + (line) => + line.includes("Info:") && line.includes("does not affect `inspect --view engine`"), + ), + ).toHaveLength(1); + const parsed = JSON.parse(output.stdout[0] ?? "{}"); + expect(parsed.view).toBe("engine"); + expect(parsed.detector).toBe("wasm"); + }); + + test("emits the engine-view info note for inspect.contentGate.mode derived from config", async () => { + if (!hasWasmDetectorRuntime()) { + return; + } + + const cwd = await makeTempFixture("inspect-engine-content-gate-inspect-config-note"); + await writeTomlConfig(cwd, [ + 'detector = "wasm"', + "", + "[inspect.contentGate]", + 'mode = "strict"', + ]); + + const output = await captureCli( + ["inspect", "--view", "engine", "--format", "json", inspectContentGateText], + { cwd }, + ); + + expect(output.exitCode).toBe(0); + expect( + output.stderr.filter( + (line) => + line.includes("Info:") && line.includes("does not affect `inspect --view engine`"), + ), + ).toHaveLength(1); + const parsed = JSON.parse(output.stdout[0] ?? "{}"); + expect(parsed.view).toBe("engine"); + expect(parsed.detector).toBe("wasm"); + }); + test("lets inspect inherit root contentGate.mode when inspect.contentGate.mode is absent", async () => { if (!hasWasmDetectorRuntime()) { return; @@ -122,6 +177,36 @@ describe("CLI inspect config overrides and defaults", () => { }); }); + test("emits the engine-view info note for env-derived non-default content-gate mode", async () => { + if (!hasWasmDetectorRuntime()) { + return; + } + + const cwd = await makeTempFixture("inspect-engine-content-gate-env-note"); + await writeTomlConfig(cwd, ['detector = "wasm"']); + + const output = await captureCli( + ["inspect", "--view", "engine", "--format", "json", inspectContentGateText], + { + cwd, + env: { + WORD_COUNTER_CONTENT_GATE: "loose", + }, + }, + ); + + expect(output.exitCode).toBe(0); + expect( + output.stderr.filter( + (line) => + line.includes("Info:") && line.includes("does not affect `inspect --view engine`"), + ), + ).toHaveLength(1); + const parsed = JSON.parse(output.stdout[0] ?? "{}"); + expect(parsed.view).toBe("engine"); + expect(parsed.detector).toBe("wasm"); + }); + test("lets inspect.contentGate.mode override the root content gate without changing counting defaults", async () => { if (!hasWasmDetectorRuntime()) { return; diff --git a/test/command-inspect.test.ts b/test/command-inspect.test.ts index 1842e1c..907af06 100644 --- a/test/command-inspect.test.ts +++ b/test/command-inspect.test.ts @@ -42,6 +42,92 @@ describe("inspect command", () => { expect(output.stdout.some((line) => line.includes("Route tag: und-Hani"))).toBeTrue(); }); + test("does not emit an engine-view info note for inherited default content-gate mode", async () => { + if (!hasWasmDetectorRuntime()) { + return; + } + + const output = await captureCli([ + "inspect", + "--detector", + "wasm", + "--view", + "engine", + "This sentence should clearly be detected as English for the wasm detector path.", + ]); + + expect(output.exitCode).toBe(0); + expect( + output.stderr.some( + (line) => + line.includes("Info:") && line.includes("does not affect `inspect --view engine`"), + ), + ).toBeFalse(); + }); + + test("emits an engine-view info note for explicit --content-gate default", async () => { + if (!hasWasmDetectorRuntime()) { + return; + } + + const output = await captureCli([ + "inspect", + "--detector", + "wasm", + "--view", + "engine", + "--content-gate", + "default", + "This sentence should clearly be detected as English for the wasm detector path.", + ]); + + expect(output.exitCode).toBe(0); + expect( + output.stderr.filter( + (line) => + line.includes("Info:") && line.includes("does not affect `inspect --view engine`"), + ), + ).toHaveLength(1); + expect( + output.stderr.some( + (line) => + line.includes("Info:") && line.includes("Use `--view pipeline` to inspect eligibility"), + ), + ).toBeTrue(); + }); + + test("keeps engine-view JSON output on stdout while emitting the info note on stderr", async () => { + if (!hasWasmDetectorRuntime()) { + return; + } + + const output = await captureCli([ + "inspect", + "--detector", + "wasm", + "--view", + "engine", + "--content-gate", + "strict", + "--format", + "json", + "Readers understand this behavior.", + ]); + + expect(output.exitCode).toBe(0); + expect(output.stdout.length).toBe(1); + expect(output.stdout[0]?.includes("does not affect `inspect --view engine`")).toBeFalse(); + expect( + output.stderr.filter( + (line) => + line.includes("Info:") && line.includes("does not affect `inspect --view engine`"), + ), + ).toHaveLength(1); + const parsed = JSON.parse(output.stdout[0] ?? "{}"); + expect(parsed.view).toBe("engine"); + expect(parsed.detector).toBe("wasm"); + }); + test("bounds standard engine inspection output to previews", async () => { if (!hasWasmDetectorRuntime()) { return; @@ -232,6 +318,46 @@ describe("inspect command", () => { ).toBeTrue(); }); + test("emits the engine-view info note once per batch inspect invocation", async () => { + if (!hasWasmDetectorRuntime()) { + return; + } + + const root = await makeTempFixture("inspect-engine-content-gate-batch-note"); + const firstPath = join(root, "a.txt"); + const secondPath = join(root, "b.txt"); + const text = "This sentence should clearly be detected as English for the wasm detector path."; + await writeFile(firstPath, text); + await writeFile(secondPath, text); + + const output = await captureCli([ + "inspect", + "--detector", + "wasm", + "--view", + "engine", + "--content-gate", + "off", + "--format", + "json", + "--path", + firstPath, + "--path", + secondPath, + ]); + + expect(output.exitCode).toBe(0); + expect( + output.stderr.filter( + (line) => + line.includes("Info:") && line.includes("does not affect `inspect --view engine`"), + ), + ).toHaveLength(1); + const parsed = JSON.parse(output.stdout[0] ?? "{}"); + expect(parsed.kind).toBe("detector-inspect-batch"); + expect(parsed.files).toHaveLength(2); + }); + test("shows strict inspect eligibility and content gate details in standard output", async () => { if (!hasWasmDetectorRuntime()) { return;