yam-flow 2.8.3 → 2.8.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +4 -0
- package/CHANGELOG.md +31 -0
- package/COMMANDS.md +6 -0
- package/README.md +8 -5
- package/dist/bin/yam.js +24 -5
- package/dist/lib/external-updates.js +31 -12
- package/dist/lib/security-integration.js +10 -1
- package/dist/lib/test-check.d.ts +156 -0
- package/dist/lib/test-check.js +558 -0
- package/package.json +5 -2
- package/references/final-report.md +4 -0
- package/references/scout-wide-scan.md +16 -0
- package/references/test-check.md +192 -0
- package/skills/deep/SKILL.md +4 -0
- package/skills/mission/SKILL.md +4 -0
- package/skills/quick/SKILL.md +4 -0
- package/skills/scout/SKILL.md +3 -0
- package/skills/ueye/SKILL.md +4 -0
- package/src/bin/yam.ts +19 -5
- package/src/lib/external-updates.ts +32 -13
- package/src/lib/security-integration.ts +9 -1
- package/src/lib/test-check.ts +348 -0
- package/templates/test-check-policy.json +53 -0
- package/yam.manifest.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -24,6 +24,10 @@
|
|
|
24
24
|
22. When an artifact changed, put `Next step` immediately after Study Note. Ordinary advisory work may use a short concrete action or state that none remains. For risky changes, explicit strict reports and structured receipts, build it from a quick whole-process scan with current situation, forward outlook, a concrete critical opinion, improvement recommendations, and ordered fix-first then planned actions with evidence and ownership. Keep report validation separate from actual security verification.
|
|
25
25
|
23. Work on the current branch by default. Create or switch branches, create worktrees, or create pull requests only when the user explicitly requests that action or an applicable project rule explicitly requires it. A commit/push request, release version bump, route selection, branch-name prefix, or branch protection alone is not authorization. If protection blocks the requested operation, report the blocker and ask for direction; do not silently choose a PR workflow, bypass checks, or change protection settings.
|
|
26
26
|
|
|
27
|
+
## Test check completion gate
|
|
28
|
+
|
|
29
|
+
After implementation and before claiming changed work complete, follow `references/test-check.md`: identify the exact starting revision, map changed targets to required scenarios, run the project Test check adapter and require a current PASS plus its explained note. Missing real DB/runtime evidence, skipped required scenarios, stale source/environment or note-write failures block completion approval. Report `Test check: BLOCKED` for an incomplete handoff; it does not grant commit/push/deployment approval. Recheck source identity before those operations. Do not treat report formatting, mock results, or operator-declared receipts as actual runtime verification. Projects need their own policy and adapters; an unavailable configuration is not a security or completion approval.
|
|
30
|
+
|
|
27
31
|
## Security check gate
|
|
28
32
|
|
|
29
33
|
Before commit, push, CI approval, or deployment, run the configured `yam security check` gate for the exact stage and record its Security check block. Use `references/security-check.md`; preserve every SEC explanation, evidence reference, and unresolved status. Missing required evidence or a note-write failure blocks the stage. Local hooks are bypassable; enforce protected merge/deployment with a trusted CI policy and required checks. Never present a validated supplied report as an independent vulnerability scan.
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,37 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to yam-flow are documented here.
|
|
4
4
|
|
|
5
|
+
## v2.8.5 - 2026-10-28
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Add the Test check completion gate with project-owned scenario policies, measured subprocess observations, source/environment bindings and private append-only notes.
|
|
10
|
+
- Require current Test check evidence before configured commit, push, CI and deployment security approval; CI verifies an administrator-reviewed test policy pin before executing project code.
|
|
11
|
+
- Add an optional isolated PostgreSQL simulation that reproduces SQLSTATE 42702 and checks normal writes, authorization, rollback, migration freshness and function drift using synthetic data.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- Keep private `.yam` research receipts and temporary tools outside the public-product terminology scan while retaining checks for public source and documentation.
|
|
16
|
+
|
|
17
|
+
### Verification
|
|
18
|
+
|
|
19
|
+
- 49 gate regression cases and full self-verification passed. Real PostgreSQL 17.10 simulations cover ten workflow scenarios and a 16-case role/brand/quantity matrix.
|
|
20
|
+
- Exact-target security integration and package checks passed locally. Production application behavior and hosted rollout require their own project evidence.
|
|
21
|
+
|
|
22
|
+
## v2.8.4 - 2026-10-26
|
|
23
|
+
|
|
24
|
+
### Improved
|
|
25
|
+
|
|
26
|
+
- Scrapling updates select the browser CLI for 0.4 or 0.5+, preflight candidate version/help before browser dependency installation, and preserve the existing atomic-link/rollback boundary.
|
|
27
|
+
- Update smokes extract the document title instead of relying on a body heading; a zero exit with missing expected content is recorded as a failed check.
|
|
28
|
+
- Scout guidance connects consequential claims to inspected evidence, separates reported/tested/inferred conclusions, retains version conflicts and adds cheap falsification checks without mandatory reviewers or a new receipt schema.
|
|
29
|
+
- Research guidance makes outbound-data authority and sensitive session/network-state handling explicit; updater documentation distinguishes recorded PyPI hashes from a fully hash-locked dependency installation.
|
|
30
|
+
|
|
31
|
+
### Verification
|
|
32
|
+
|
|
33
|
+
- Focused update fixtures cover 0.4 compatibility, 0.4.15-to-0.5 variants, missing CLI/browser failures, post-switch failure and receipt-write rollback.
|
|
34
|
+
- Isolated macOS/Python 3.14 checks exercised actual 0.4.15-to-0.5 installation, HTTP/headless extraction and receipt-failure rollback. A later isolated public suffix-list retry passed after a local CA-path repair; that environment-only repair is not shipped by this package.
|
|
35
|
+
|
|
5
36
|
## v2.8.3 - 2026-10-06
|
|
6
37
|
|
|
7
38
|
### Improved
|
package/COMMANDS.md
CHANGED
|
@@ -176,6 +176,8 @@ yam update apply --all --json
|
|
|
176
176
|
|
|
177
177
|
`yam update check` is read-only. Its component result preserves the registry artifact source receipt: npm integrity/`gitHead` for yam, PyPI release-file SHA-256 values for Scrapling, and the Git-pinned manifest commit for Insane Search. Yam also compares `latest` with exact-version metadata and probes the official exact tarball URL; a 404 is `pending_distribution`, while network/auth/metadata mismatch is `check_failed`. The bounded HEAD probe confirms availability only and does not hash tarball bytes. Every `apply` needs explicit authorization, pins the official npm registry or isolated PyPI index, uses a concurrent-run lock, records a component receipt, and stops on failed/manual results. `--all` runs yam last. A yam install verifies the effective executable identity before any yam-side install command and repeats that check during rollback. This identity contract currently supports POSIX symlink layouts; Windows npm `.cmd` shims fail closed as unverified. Insane Search requires a clean tracked local manifest whose bytes match Git and matching pre/post-add commits, is never remove-first, and never edits `.codex/plugins/cache` directly.
|
|
178
178
|
|
|
179
|
+
Scrapling preflights the candidate version and browser CLI before installing browser dependencies, then uses `extract stealthy-fetch` for 0.4 or `extract fetch` for 0.5+. HTTP/browser smoke checks must pass before the managed link changes; failures retain or restore the previous link. Recorded PyPI hashes do not imply a complete hash-locked dependency install; see README's hash-enforcement boundary.
|
|
180
|
+
|
|
179
181
|
## Release Supply Chain
|
|
180
182
|
|
|
181
183
|
```bash
|
|
@@ -351,3 +353,7 @@ yam hook disable study-note --global
|
|
|
351
353
|
- `yam template security` / `yam template security-ci`: explained note and CI setup templates.
|
|
352
354
|
|
|
353
355
|
See `references/security-check.md` for stage arguments, trusted policy requirements and limitations. This validates supplied evidence; it does not automatically scan application vulnerabilities.
|
|
356
|
+
|
|
357
|
+
## Test check
|
|
358
|
+
|
|
359
|
+
Before changed-work completion: `yam test-check plan --base <exact starting commit> --json`, then `yam test-check run --base <exact starting commit> --json` and `yam test-check check --json`. Configure `testing/policy.json` or `.yam/testing/policy.json` and a reviewed JSON-report adapter first. `yam template test-check` prints a blocked-until-configured starter. See `references/test-check.md` for scenario floors, real DB manifests, Stop integration and stage-specific identity checks. `check --stage commit` verifies index equality; push/CI/deploy require `--source <exact commit>` and CI/deploy also require a trusted `--policy-digest`. No global hook, branch protection or deployment is silently installed.
|
package/README.md
CHANGED
|
@@ -177,7 +177,9 @@ A yam update is successful only after the installed version and status checks pl
|
|
|
177
177
|
|
|
178
178
|
Effective yam identity currently verifies POSIX-style executable/symlink layouts through `which` and canonical realpaths. Windows npm `.cmd` shim identity is intentionally unverified and fails closed until a dedicated shim parser and fixture exist.
|
|
179
179
|
|
|
180
|
-
Scrapling updates install an exact stable PyPI version into a new versioned virtual environment. `pip check`, ordinary HTTP extraction, and headless browser extraction must pass before the executable symlink changes atomically; the previous environment is retained. An existing environment must have a matching `.yam-scrapling-install.json` ownership marker before yam will replace its link. Insane Search uses only the official `codex plugin` commands. yam never edits `.codex/plugins/cache`, never removes the plugin first, and returns `manual_plugin_update_required` when the installed Codex CLI cannot update it safely in place.
|
|
180
|
+
Scrapling updates install an exact stable PyPI version into a new versioned virtual environment. Candidate version and browser-command help are checked before browser dependency installation: 0.4 uses `extract stealthy-fetch`, while 0.5 and later use `extract fetch`. An unavailable command fails without trying legacy aliases. `pip check`, ordinary HTTP extraction, and headless browser extraction must pass before the executable symlink changes atomically; the previous environment is retained. An existing environment must have a matching `.yam-scrapling-install.json` ownership marker before yam will replace its link. Insane Search uses only the official `codex plugin` commands. yam never edits `.codex/plugins/cache`, never removes the plugin first, and returns `manual_plugin_update_required` when the installed Codex CLI cannot update it safely in place.
|
|
181
|
+
|
|
182
|
+
Scrapling hash-enforcement boundary: the check receipt records PyPI release-file SHA-256 values, but apply does not bind pip's selected artifact to that prior receipt or lock transitive dependencies. Native index hashes and TLS are not an independently reviewed lock. Full `--require-hashes` enforcement needs exact versions and accepted hashes for every dependency, plus Python/platform-specific wheel and rollback coverage; adding a single top-level hash is not sufficient. That installer change remains deferred rather than claiming stronger supply-chain proof. See [pip secure installs](https://pip.pypa.io/en/stable/topics/secure-installs/).
|
|
181
183
|
|
|
182
184
|
There is no unattended updater. The exact project trigger in `AGENTS.md` grants one-time authorization for a checked `--all` run, and Codex is never restarted automatically.
|
|
183
185
|
|
|
@@ -198,8 +200,9 @@ The isolated executable can be used directly from the shell:
|
|
|
198
200
|
|
|
199
201
|
```bash
|
|
200
202
|
scrapling --version
|
|
201
|
-
scrapling extract get https://example.com example.txt --css-selector
|
|
202
|
-
|
|
203
|
+
scrapling extract get https://example.com example.txt --css-selector title
|
|
204
|
+
# Scrapling 0.5+ (use stealthy-fetch instead of fetch on 0.4)
|
|
205
|
+
scrapling extract fetch https://example.com example-browser.txt --css-selector title --headless
|
|
203
206
|
```
|
|
204
207
|
|
|
205
208
|
From Python inside the installed environment:
|
|
@@ -208,10 +211,10 @@ From Python inside the installed environment:
|
|
|
208
211
|
from scrapling.fetchers import Fetcher, StealthyFetcher
|
|
209
212
|
|
|
210
213
|
page = Fetcher.get("https://example.com")
|
|
211
|
-
print(page.css("
|
|
214
|
+
print(page.css("title::text").get())
|
|
212
215
|
|
|
213
216
|
browser_page = StealthyFetcher.fetch("https://example.com", headless=True)
|
|
214
|
-
print(browser_page.css("
|
|
217
|
+
print(browser_page.css("title::text").get())
|
|
215
218
|
```
|
|
216
219
|
|
|
217
220
|
Use `Fetcher` for ordinary HTTP pages and `StealthyFetcher` only when a real browser or stronger anti-bot behavior is needed; the browser path is heavier.
|
package/dist/bin/yam.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { runTestCheckCommand, testCheckStop, testCheckConfigured } from '../lib/test-check.js';
|
|
2
3
|
import { spawnSync } from 'node:child_process';
|
|
3
4
|
import { studyNoteTurnScope } from '../lib/study-note-scope.js';
|
|
4
5
|
import { selectHookNode, probeHookDiscovery, inspectHookObservation, recordHookObservation } from '../lib/hook-readiness.js';
|
|
@@ -149,6 +150,7 @@ Usage:
|
|
|
149
150
|
yam proof [dir|--from file] [--route route] [--truth status] [--command text] [--evidence text]
|
|
150
151
|
yam proof write [dir] [--format json|md] [--out file] [--route route] [--truth status] [--command text]
|
|
151
152
|
yam study-note check [dir] [--report file|--text text] [--json]
|
|
153
|
+
yam test-check <plan|run|check|status> [--base commit] [--json]
|
|
152
154
|
yam security <init|check|hooks|status> [--stage commit|push|ci|deploy] [--json]
|
|
153
155
|
yam next-step <report|verify> [--spec file|--receipt file] [--json]
|
|
154
156
|
yam loop report [--route route] [--intent text] [--stage id:status:note] [--evidence text] [--json]
|
|
@@ -175,7 +177,7 @@ Usage:
|
|
|
175
177
|
yam safety [text...]
|
|
176
178
|
yam memory <init|add|list|summary|resolve> [dir] [options]
|
|
177
179
|
yam hook <status|enable|disable|run> [lite|study-note] [--global|--project dir]
|
|
178
|
-
yam template <project|ueye|mission|proof|tuning|security|security-ci>
|
|
180
|
+
yam template <project|ueye|mission|proof|tuning|security|security-ci|test-check>
|
|
179
181
|
yam tune-log [dir]
|
|
180
182
|
yam install [--dry-run] [--json] [--replace-user-skill name]
|
|
181
183
|
yam uninstall
|
|
@@ -495,6 +497,7 @@ async function verify({ quiet = false } = {}) {
|
|
|
495
497
|
'markdown-management.md',
|
|
496
498
|
'study-note.md',
|
|
497
499
|
'security-check.md',
|
|
500
|
+
'test-check.md',
|
|
498
501
|
'final-report.md',
|
|
499
502
|
'token-budget-reporter.md',
|
|
500
503
|
'memory.md'
|
|
@@ -513,7 +516,7 @@ async function verify({ quiet = false } = {}) {
|
|
|
513
516
|
issues.push(`project template missing section: ${section}`);
|
|
514
517
|
}
|
|
515
518
|
}
|
|
516
|
-
for (const template of ['ueye-review.md', 'ueye-comparison.md', 'mission-plan.md', 'runtime-proof.md', 'tuning-log.md', 'security-check-note.md', 'security-gate.yml']) {
|
|
519
|
+
for (const template of ['ueye-review.md', 'ueye-comparison.md', 'mission-plan.md', 'runtime-proof.md', 'tuning-log.md', 'security-check-note.md', 'security-gate.yml', 'test-check-policy.json']) {
|
|
517
520
|
if (!await exists(path.join(ROOT, 'templates', template)))
|
|
518
521
|
issues.push(`missing template: ${template}`);
|
|
519
522
|
}
|
|
@@ -2477,6 +2480,9 @@ async function buildYamLiteContext({ cwd, prompt }) {
|
|
|
2477
2480
|
async function buildStudyNoteStopOutput(input = {}, cwd = process.cwd()) {
|
|
2478
2481
|
const lastAssistantMessage = String(input?.last_assistant_message || input?.lastAssistantMessage || '');
|
|
2479
2482
|
const scope = studyNoteTurnScope(input, cwd);
|
|
2483
|
+
const testGate = testCheckStop(cwd, !scope.available || scope.files.length > 0, lastAssistantMessage);
|
|
2484
|
+
if (testGate)
|
|
2485
|
+
return testGate;
|
|
2480
2486
|
const result = await buildStudyNoteGuardResult(path.resolve(cwd), lastAssistantMessage, 'stop_hook_last_assistant_message', scope);
|
|
2481
2487
|
if (result.truth_status === 'partial' && !result.changed_file_detection.available) {
|
|
2482
2488
|
return {
|
|
@@ -2507,9 +2513,10 @@ async function buildStudyNoteStopOutput(input = {}, cwd = process.cwd()) {
|
|
|
2507
2513
|
}
|
|
2508
2514
|
async function buildStudyNoteTurnContext(input, cwd) {
|
|
2509
2515
|
const scope = studyNoteTurnScope(input, cwd, true);
|
|
2516
|
+
const testReminder = testCheckConfigured(cwd) ? ' Test check active: before completion run yam test-check run --base <exact starting commit> --json and yam test-check check --json. Missing required tests/evidence block completion; report Test check: BLOCKED if unable to verify. This is separate from report formatting and Security check.' : '';
|
|
2510
2517
|
if (!scope.available)
|
|
2511
2518
|
return 'yam Study Note: turn scope is unavailable; do not treat this as a clean project. Inspect changed artifacts manually; no strict block will be applied without a baseline.';
|
|
2512
|
-
return `yam Study Note turn baseline saved (hash-only). Mode: ${scope.mode}. Only changes since this prompt count; known generated logs, mission/security receipts and screenshots are excluded unless explicitly included. Include a concise Study Note followed by Next step when product artifacts change. ${scope.mode === 'strict' ? 'Risk work uses the detailed report contract and one correction pass.' : 'Ordinary work is advisory: a short explanation, verification/limits and remaining work (or none) suffice.'}`;
|
|
2519
|
+
return `yam Study Note turn baseline saved (hash-only). Mode: ${scope.mode}. Only changes since this prompt count; known generated logs, mission/security receipts and screenshots are excluded unless explicitly included. Include a concise Study Note followed by Next step when product artifacts change. ${scope.mode === 'strict' ? 'Risk work uses the detailed report contract and one correction pass.' : 'Ordinary work is advisory: a short explanation, verification/limits and remaining work (or none) suffice.'}${testReminder}`;
|
|
2513
2520
|
}
|
|
2514
2521
|
function yamLiteRouteHint(prompt = '') {
|
|
2515
2522
|
const text = String(prompt || '').toLowerCase();
|
|
@@ -6132,11 +6139,12 @@ async function printTemplate(name = '') {
|
|
|
6132
6139
|
runtime: 'runtime-proof.md',
|
|
6133
6140
|
tuning: 'tuning-log.md',
|
|
6134
6141
|
security: 'security-check-note.md',
|
|
6135
|
-
'security-ci': 'security-gate.yml'
|
|
6142
|
+
'security-ci': 'security-gate.yml',
|
|
6143
|
+
'test-check': 'test-check-policy.json'
|
|
6136
6144
|
};
|
|
6137
6145
|
const file = map[key];
|
|
6138
6146
|
if (!file) {
|
|
6139
|
-
console.error('usage: yam template <project|ueye|ueye-comparison|mission|proof|tuning|security|security-ci>');
|
|
6147
|
+
console.error('usage: yam template <project|ueye|ueye-comparison|mission|proof|tuning|security|security-ci|test-check>');
|
|
6140
6148
|
process.exitCode = 1;
|
|
6141
6149
|
return;
|
|
6142
6150
|
}
|
|
@@ -6433,6 +6441,17 @@ function showRequestedHelp(args) {
|
|
|
6433
6441
|
async function main() {
|
|
6434
6442
|
const args = process.argv.slice(2);
|
|
6435
6443
|
const command = args[0] || 'help';
|
|
6444
|
+
if (command === 'test-check') {
|
|
6445
|
+
if (!args[1] || ['help', '--help', '-h'].includes(args[1])) {
|
|
6446
|
+
console.log('yam test-check run --base <exact commit> [--policy file --policy-digest sha256] [--json]\nyam test-check check [--stage complete|commit|push|ci|deploy --source <exact commit> --policy-digest sha256 --receipt file] [--json]\nyam test-check status\nyam test-check plan --base <exact commit> --json\nSee references/test-check.md for required scenarios, adapters and activation.');
|
|
6447
|
+
return;
|
|
6448
|
+
}
|
|
6449
|
+
const result = runTestCheckCommand(args.slice(1));
|
|
6450
|
+
console.log(JSON.stringify(result, null, 2));
|
|
6451
|
+
if (!result.ok)
|
|
6452
|
+
process.exitCode = 1;
|
|
6453
|
+
return;
|
|
6454
|
+
}
|
|
6436
6455
|
if (showRequestedHelp(args))
|
|
6437
6456
|
return;
|
|
6438
6457
|
if (command === 'help')
|
|
@@ -767,6 +767,20 @@ async function applyScrapling(deps) {
|
|
|
767
767
|
checks.push(commandCheck('install_exact_scrapling', install));
|
|
768
768
|
if (!install.ok)
|
|
769
769
|
throw new Error(`Scrapling install failed: ${commandNote(install)}`);
|
|
770
|
+
const directVersion = await deps.run(candidateScrapling, ['--version'], { env: deps.env, timeout: 30000 });
|
|
771
|
+
checks.push(commandCheck('candidate_version', directVersion));
|
|
772
|
+
if (!directVersion.ok || extractVersion(directVersion.stdout) !== check.latest_version) {
|
|
773
|
+
throw new Error(`candidate Scrapling version mismatch: ${commandNote(directVersion)}`);
|
|
774
|
+
}
|
|
775
|
+
const browserCommand = compareVersions(check.latest_version, '0.5') >= 0 ? 'fetch' : 'stealthy-fetch';
|
|
776
|
+
const browserHelp = await deps.run(candidateScrapling, ['extract', browserCommand, '--help'], {
|
|
777
|
+
env: deps.env,
|
|
778
|
+
timeout: 30000
|
|
779
|
+
});
|
|
780
|
+
checks.push(commandCheck('browser_cli_compatibility', browserHelp));
|
|
781
|
+
if (!browserHelp.ok) {
|
|
782
|
+
throw new Error(`Scrapling browser CLI ${browserCommand} is unavailable: ${commandNote(browserHelp)}`);
|
|
783
|
+
}
|
|
770
784
|
const browsers = await deps.run(candidateScrapling, ['install', '--force'], {
|
|
771
785
|
env: deps.env,
|
|
772
786
|
timeout: 600000
|
|
@@ -785,27 +799,32 @@ async function applyScrapling(deps) {
|
|
|
785
799
|
const httpOutput = path.join(candidate, '.yam-http-smoke.txt');
|
|
786
800
|
const browserOutput = path.join(candidate, '.yam-browser-smoke.txt');
|
|
787
801
|
const httpSmoke = await deps.run(candidateScrapling, [
|
|
788
|
-
'extract', 'get', 'https://example.com', httpOutput, '--css-selector', '
|
|
802
|
+
'extract', 'get', 'https://example.com', httpOutput, '--css-selector', 'title', '--timeout', '30'
|
|
789
803
|
], { env: deps.env, timeout: 120000 });
|
|
790
|
-
|
|
791
|
-
|
|
804
|
+
const httpContentOk = httpSmoke.ok && await fileContains(httpOutput, 'Example Domain');
|
|
805
|
+
checks.push({
|
|
806
|
+
...commandCheck('http_smoke', httpSmoke),
|
|
807
|
+
status: httpContentOk ? 'passed' : 'failed',
|
|
808
|
+
note: httpContentOk ? commandNote(httpSmoke) : boundedNote(`expected extracted title was not confirmed; ${commandNote(httpSmoke)}`)
|
|
809
|
+
});
|
|
810
|
+
if (!httpContentOk) {
|
|
792
811
|
throw new Error(`Scrapling HTTP smoke failed: ${commandNote(httpSmoke)}`);
|
|
793
812
|
}
|
|
794
813
|
const browserSmoke = await deps.run(candidateScrapling, [
|
|
795
|
-
'extract',
|
|
796
|
-
'--css-selector', '
|
|
814
|
+
'extract', browserCommand, 'https://example.com', browserOutput,
|
|
815
|
+
'--css-selector', 'title', '--headless', '--timeout', '30000'
|
|
797
816
|
], { env: deps.env, timeout: 180000 });
|
|
798
|
-
|
|
799
|
-
|
|
817
|
+
const browserContentOk = browserSmoke.ok && await fileContains(browserOutput, 'Example Domain');
|
|
818
|
+
checks.push({
|
|
819
|
+
...commandCheck('browser_smoke', browserSmoke),
|
|
820
|
+
status: browserContentOk ? 'passed' : 'failed',
|
|
821
|
+
note: browserContentOk ? commandNote(browserSmoke) : boundedNote(`expected extracted title was not confirmed; ${commandNote(browserSmoke)}`)
|
|
822
|
+
});
|
|
823
|
+
if (!browserContentOk) {
|
|
800
824
|
throw new Error(`Scrapling browser smoke failed: ${commandNote(browserSmoke)}`);
|
|
801
825
|
}
|
|
802
826
|
await unlinkIfExists(httpOutput);
|
|
803
827
|
await unlinkIfExists(browserOutput);
|
|
804
|
-
const directVersion = await deps.run(candidateScrapling, ['--version'], { env: deps.env, timeout: 30000 });
|
|
805
|
-
checks.push(commandCheck('candidate_version', directVersion));
|
|
806
|
-
if (!directVersion.ok || extractVersion(directVersion.stdout) !== check.latest_version) {
|
|
807
|
-
throw new Error(`candidate Scrapling version mismatch: ${commandNote(directVersion)}`);
|
|
808
|
-
}
|
|
809
828
|
await fsp.writeFile(path.join(candidate, '.yam-scrapling-install.json'), `${JSON.stringify({
|
|
810
829
|
schema: 'yam.scrapling-install.v1',
|
|
811
830
|
version: check.latest_version,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { checkTestCheck, testCheckConfigured } from './test-check.js';
|
|
1
2
|
import { createHash, randomUUID } from 'node:crypto';
|
|
2
3
|
import { execFileSync } from 'node:child_process';
|
|
3
4
|
import fs from 'node:fs/promises';
|
|
@@ -30,7 +31,7 @@ function parseArgs(args) {
|
|
|
30
31
|
for (let i = 0; i < args.length; i++) {
|
|
31
32
|
if (args[i] === '--json')
|
|
32
33
|
continue;
|
|
33
|
-
if (!['--policy', '--policy-digest', '--evidence', '--note', '--stage', '--remote', '--destination', '--source', '--base', '--artifact', '--artifact-digest', '--configuration', '--configuration-digest', '--environment', '--cli'].includes(args[i]) || !args[i + 1] || args[i + 1].startsWith('--') || out.has(args[i]))
|
|
34
|
+
if (!['--policy', '--policy-digest', '--evidence', '--note', '--stage', '--remote', '--destination', '--source', '--base', '--artifact', '--artifact-digest', '--configuration', '--configuration-digest', '--environment', '--cli', '--test-policy-digest'].includes(args[i]) || !args[i + 1] || args[i + 1].startsWith('--') || out.has(args[i]))
|
|
34
35
|
throw new Error('invalid or duplicate security option');
|
|
35
36
|
out.set(args[i], args[++i]);
|
|
36
37
|
}
|
|
@@ -381,6 +382,14 @@ async function check(cwd, options, settings) {
|
|
|
381
382
|
result = validateSecurityCheck(input, binding);
|
|
382
383
|
gateErrors.push('security_input_or_binding_unavailable');
|
|
383
384
|
}
|
|
385
|
+
if (testCheckConfigured(cwd) || config.test_policy_digest) {
|
|
386
|
+
const pin = options.get('--test-policy-digest') || config.test_policy_digest;
|
|
387
|
+
const sources = stage === 'push' ? (binding.push_refs || []).map(ref => ref.local_oid)
|
|
388
|
+
: [binding.source_revision];
|
|
389
|
+
const targets = sources.length ? sources : [undefined];
|
|
390
|
+
if (targets.some(source => !checkTestCheck(cwd, { stage, source, pin, base: stage === 'ci' ? binding.base_revision : undefined, artifact: options.get('--artifact'), configuration: options.get('--configuration') }).ok))
|
|
391
|
+
gateErrors.push('test_check_blocked');
|
|
392
|
+
}
|
|
384
393
|
if (gateErrors.length) {
|
|
385
394
|
result.result = 'BLOCKED';
|
|
386
395
|
result.errors = [...new Set([...result.errors, ...gateErrors])];
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
export declare const TEST_REQUIREMENTS: {
|
|
2
|
+
readonly docs: readonly ["inspection"];
|
|
3
|
+
readonly logic: readonly ["success", "invalid", "boundary"];
|
|
4
|
+
readonly api: readonly ["success", "invalid", "contract"];
|
|
5
|
+
readonly db: readonly ["success", "denied", "invalid", "readback", "rollback", "migration_upgrade", "migration_fresh"];
|
|
6
|
+
readonly auth: readonly ["success", "denied", "cross_scope", "revoked"];
|
|
7
|
+
readonly ui: readonly ["success", "error"];
|
|
8
|
+
readonly release: readonly ["artifact", "integrity"];
|
|
9
|
+
};
|
|
10
|
+
type Risk = keyof typeof TEST_REQUIREMENTS;
|
|
11
|
+
type Check = {
|
|
12
|
+
id: string;
|
|
13
|
+
target: string;
|
|
14
|
+
scenario: string;
|
|
15
|
+
explanation: string;
|
|
16
|
+
evidence: 'static' | 'unit' | 'integration' | 'e2e';
|
|
17
|
+
command: string[];
|
|
18
|
+
environment?: string;
|
|
19
|
+
};
|
|
20
|
+
type Surface = {
|
|
21
|
+
id: string;
|
|
22
|
+
paths: string[];
|
|
23
|
+
risks: Risk[];
|
|
24
|
+
targets: string[];
|
|
25
|
+
checks: Check[];
|
|
26
|
+
};
|
|
27
|
+
type Policy = {
|
|
28
|
+
schema: 'yam.test-policy.v1';
|
|
29
|
+
surfaces: Surface[];
|
|
30
|
+
};
|
|
31
|
+
export declare function planTestCheck(root: string, policy: Policy, base: string): {
|
|
32
|
+
changed: string[];
|
|
33
|
+
source_digest: string;
|
|
34
|
+
checks: Check[];
|
|
35
|
+
errors: string[];
|
|
36
|
+
};
|
|
37
|
+
export declare function runTestCheck(cwd: string, options?: {
|
|
38
|
+
policy?: string;
|
|
39
|
+
pin?: string;
|
|
40
|
+
base?: string;
|
|
41
|
+
stage?: string;
|
|
42
|
+
source?: string;
|
|
43
|
+
artifact?: string;
|
|
44
|
+
configuration?: string;
|
|
45
|
+
}): {
|
|
46
|
+
ok: boolean;
|
|
47
|
+
result: string;
|
|
48
|
+
errors: any[];
|
|
49
|
+
run?: undefined;
|
|
50
|
+
note?: undefined;
|
|
51
|
+
receipt?: undefined;
|
|
52
|
+
changed_files?: undefined;
|
|
53
|
+
checks?: undefined;
|
|
54
|
+
} | {
|
|
55
|
+
ok: boolean;
|
|
56
|
+
result: any;
|
|
57
|
+
errors: any;
|
|
58
|
+
run: any;
|
|
59
|
+
note: string;
|
|
60
|
+
receipt: string;
|
|
61
|
+
changed_files: any;
|
|
62
|
+
checks: number;
|
|
63
|
+
};
|
|
64
|
+
export declare function checkTestCheck(cwd: string, options?: {
|
|
65
|
+
policy?: string;
|
|
66
|
+
pin?: string;
|
|
67
|
+
stage?: string;
|
|
68
|
+
source?: string;
|
|
69
|
+
receipt?: string;
|
|
70
|
+
artifact?: string;
|
|
71
|
+
configuration?: string;
|
|
72
|
+
base?: string;
|
|
73
|
+
}): {
|
|
74
|
+
ok: boolean;
|
|
75
|
+
result: string;
|
|
76
|
+
errors: string[];
|
|
77
|
+
run: any;
|
|
78
|
+
checks: number;
|
|
79
|
+
} | {
|
|
80
|
+
ok: boolean;
|
|
81
|
+
result: string;
|
|
82
|
+
errors: string[];
|
|
83
|
+
run?: undefined;
|
|
84
|
+
checks?: undefined;
|
|
85
|
+
};
|
|
86
|
+
export declare function testCheckConfigured(cwd: string): boolean;
|
|
87
|
+
export declare function testCheckStop(cwd: string, changed: boolean, message: string): {
|
|
88
|
+
continue: boolean;
|
|
89
|
+
systemMessage: string;
|
|
90
|
+
decision?: undefined;
|
|
91
|
+
reason?: undefined;
|
|
92
|
+
} | {
|
|
93
|
+
decision: string;
|
|
94
|
+
reason: string;
|
|
95
|
+
continue?: undefined;
|
|
96
|
+
systemMessage?: undefined;
|
|
97
|
+
};
|
|
98
|
+
export declare function runTestCheckCommand(args: string[], cwd?: string): {
|
|
99
|
+
ok: boolean;
|
|
100
|
+
result: string;
|
|
101
|
+
errors: any[];
|
|
102
|
+
run?: undefined;
|
|
103
|
+
note?: undefined;
|
|
104
|
+
receipt?: undefined;
|
|
105
|
+
changed_files?: undefined;
|
|
106
|
+
checks?: undefined;
|
|
107
|
+
} | {
|
|
108
|
+
ok: boolean;
|
|
109
|
+
result: any;
|
|
110
|
+
errors: any;
|
|
111
|
+
run: any;
|
|
112
|
+
note: string;
|
|
113
|
+
receipt: string;
|
|
114
|
+
changed_files: any;
|
|
115
|
+
checks: number;
|
|
116
|
+
} | {
|
|
117
|
+
ok: boolean;
|
|
118
|
+
result: string;
|
|
119
|
+
errors: string[];
|
|
120
|
+
run: any;
|
|
121
|
+
checks: number;
|
|
122
|
+
} | {
|
|
123
|
+
ok: boolean;
|
|
124
|
+
result: string;
|
|
125
|
+
errors: string[];
|
|
126
|
+
run?: undefined;
|
|
127
|
+
checks?: undefined;
|
|
128
|
+
} | {
|
|
129
|
+
checks: {
|
|
130
|
+
id: string;
|
|
131
|
+
target: string;
|
|
132
|
+
scenario: string;
|
|
133
|
+
explanation: string;
|
|
134
|
+
evidence: "static" | "unit" | "integration" | "e2e";
|
|
135
|
+
}[];
|
|
136
|
+
policy_digest: string;
|
|
137
|
+
changed: string[];
|
|
138
|
+
source_digest: string;
|
|
139
|
+
errors: string[];
|
|
140
|
+
ok: boolean;
|
|
141
|
+
} | {
|
|
142
|
+
ok: boolean;
|
|
143
|
+
result: string;
|
|
144
|
+
errors: string[];
|
|
145
|
+
run: any;
|
|
146
|
+
checks: number;
|
|
147
|
+
configured: boolean;
|
|
148
|
+
} | {
|
|
149
|
+
ok: boolean;
|
|
150
|
+
result: string;
|
|
151
|
+
errors: string[];
|
|
152
|
+
run?: undefined;
|
|
153
|
+
checks?: undefined;
|
|
154
|
+
configured: boolean;
|
|
155
|
+
};
|
|
156
|
+
export {};
|