canary-test-cli 7.0.0 → 7.2.0
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/skills/README.md +308 -0
- package/agents/skills/canary:generate.md +49 -0
- package/agents/skills/canary:init.md +37 -0
- package/agents/skills/canary:migrate.md +66 -0
- package/agents/skills/claude-code/canary-add-framework/SKILL.md +248 -0
- package/agents/skills/claude-code/canary-blackhawk/SKILL.md +170 -0
- package/agents/skills/claude-code/canary-blackhawk/scripts/cli.mjs +188 -0
- package/agents/skills/claude-code/canary-blackhawk/scripts/rules.mjs +120 -0
- package/agents/skills/claude-code/canary-blackhawk/scripts/scanner.mjs +244 -0
- package/agents/skills/claude-code/canary-blackhawk/scripts/string-literals.mjs +116 -0
- package/agents/skills/claude-code/canary-cassandra/SKILL.md +180 -0
- package/agents/skills/claude-code/canary-cassandra/scripts/cli.mjs +268 -0
- package/agents/skills/claude-code/canary-cassandra/scripts/engine.mjs +95 -0
- package/agents/skills/claude-code/canary-ci-ready/SKILL.md +161 -0
- package/agents/skills/claude-code/canary-ci-ready/skill.yaml +14 -0
- package/agents/skills/claude-code/canary-company-knowledge/SKILL.md +196 -0
- package/agents/skills/claude-code/canary-critical-areas/SKILL.md +142 -0
- package/agents/skills/claude-code/canary-critical-areas/skill.yaml +16 -0
- package/agents/skills/claude-code/canary-edge-case-discovery/SKILL.md +160 -0
- package/agents/skills/claude-code/canary-edge-case-discovery/skill.yaml +16 -0
- package/agents/skills/claude-code/canary-fail-fast/SKILL.md +75 -0
- package/agents/skills/claude-code/canary-fail-fast/scripts/cli.mjs +118 -0
- package/agents/skills/claude-code/canary-fail-fast/scripts/digest.mjs +69 -0
- package/agents/skills/claude-code/canary-fail-fast/scripts/failures.mjs +60 -0
- package/agents/skills/claude-code/canary-fail-fast/scripts/fastfail_check.mjs +43 -0
- package/agents/skills/claude-code/canary-fail-fast/scripts/parse.mjs +149 -0
- package/agents/skills/claude-code/canary-failure-impact/SKILL.md +153 -0
- package/agents/skills/claude-code/canary-failure-impact/skill.yaml +15 -0
- package/agents/skills/claude-code/canary-fleet-health/SKILL.md +196 -0
- package/agents/skills/claude-code/canary-generate-test/SKILL.md +185 -0
- package/agents/skills/claude-code/canary-instrument/SKILL.md +157 -0
- package/agents/skills/claude-code/canary-instrument/scripts/cli.mjs +178 -0
- package/agents/skills/claude-code/canary-instrument/scripts/otel_bootstrap/instrument.mjs +96 -0
- package/agents/skills/claude-code/canary-instrument/scripts/otel_bootstrap/playwright-fixture.ts +44 -0
- package/agents/skills/claude-code/canary-instrument/scripts/run_types.mjs +81 -0
- package/agents/skills/claude-code/canary-instrument/scripts/span_reader.mjs +187 -0
- package/agents/skills/claude-code/canary-katana/SKILL.md +243 -0
- package/agents/skills/claude-code/canary-katana/scripts/alarm.mjs +296 -0
- package/agents/skills/claude-code/canary-katana/scripts/cli.mjs +247 -0
- package/agents/skills/claude-code/canary-katana/scripts/diffscan.mjs +0 -0
- package/agents/skills/claude-code/canary-katana/scripts/ledger.mjs +183 -0
- package/agents/skills/claude-code/canary-pr-guardian/SKILL.md +129 -0
- package/agents/skills/claude-code/canary-pr-guardian/skill.yaml +17 -0
- package/agents/skills/claude-code/canary-promote-test/SKILL.md +228 -0
- package/agents/skills/claude-code/canary-savant/SKILL.md +233 -0
- package/agents/skills/claude-code/canary-savant/scripts/cli.mjs +274 -0
- package/agents/skills/claude-code/canary-savant/scripts/restoration.mjs +274 -0
- package/agents/skills/claude-code/canary-savant/scripts/rules.mjs +168 -0
- package/agents/skills/claude-code/canary-savant/scripts/runner.mjs +572 -0
- package/agents/skills/claude-code/canary-savant/scripts/scanner.mjs +374 -0
- package/agents/skills/claude-code/canary-savant/scripts/string-literals.mjs +116 -0
- package/agents/skills/claude-code/canary-setup-harness/SKILL.md +263 -0
- package/agents/skills/claude-code/canary-shadow/SKILL.md +131 -0
- package/agents/skills/claude-code/canary-shadow/scripts/cases.example.json +32 -0
- package/agents/skills/claude-code/canary-shadow/scripts/cli.mjs +195 -0
- package/agents/skills/claude-code/canary-ship/SKILL.md +177 -0
- package/agents/skills/claude-code/canary-ship/skill.yaml +16 -0
- package/agents/skills/claude-code/canary-strix/SKILL.md +130 -0
- package/agents/skills/claude-code/canary-strix/scripts/cli.mjs +255 -0
- package/agents/skills/claude-code/canary-strix/scripts/scanner.mjs +252 -0
- package/agents/skills/claude-code/canary-strix/scripts/terms.mjs +132 -0
- package/agents/skills/claude-code/canary-test-pipeline/SKILL.md +159 -0
- package/agents/skills/claude-code/canary-test-pipeline/skill.yaml +19 -0
- package/agents/skills/claude-code/canary-test-reporter/SKILL.md +138 -0
- package/agents/skills/claude-code/canary-test-reporter/scripts/cli.mjs +98 -0
- package/agents/skills/claude-code/canary-test-reporter/scripts/json_report.mjs +58 -0
- package/agents/skills/claude-code/canary-test-reporter/scripts/parse.mjs +216 -0
- package/agents/skills/claude-code/canary-test-reporter/scripts/render.mjs +114 -0
- package/agents/skills/lib/parse-args.mjs +214 -0
- package/dist/engine/analysis/cli.js +116 -54
- package/dist/engine/analysis/engine.js +34 -16
- package/dist/engine/analysis/reports.js +5 -4
- package/dist/engine/cli-commands.js +252 -106
- package/dist/engine/cli-common.js +15 -24
- package/dist/engine/cli.core.js +37 -11
- package/dist/engine/cli.js +2 -2
- package/dist/engine/company-knowledge-cli.js +2 -2
- package/dist/engine/core/adoption.js +408 -0
- package/dist/engine/core/framework-probes.js +7 -7
- package/dist/engine/core/fs-glob.js +2 -2
- package/dist/engine/core/gate-result.js +17 -0
- package/dist/engine/core/migrator.js +9 -17
- package/dist/engine/core/pattern-matcher.js +23 -5
- package/dist/engine/core/persona.js +421 -0
- package/dist/engine/core/promotion-verdict.js +261 -0
- package/dist/engine/core/quality-scorer.js +15 -2
- package/dist/engine/core/reporter.js +1 -9
- package/dist/engine/core/skill-dispatch.js +115 -0
- package/dist/engine/core/skill-examples.js +392 -0
- package/dist/engine/core/skill-registry.js +59 -4
- package/dist/engine/core/skill-surfaces.js +307 -0
- package/dist/engine/core/static-linter.js +310 -38
- package/dist/engine/core/test-files.js +77 -0
- package/dist/engine/core/ticket-updater.js +1 -7
- package/dist/engine/core/vacuity-scanner.js +726 -0
- package/dist/engine/core/workflow-discovery.js +2 -8
- package/dist/engine/core/workspace-detect.js +7 -6
- package/dist/engine/data/personas/registry.json +36 -0
- package/dist/engine/guardian/adjudication.js +5 -5
- package/dist/engine/guardian/analysis-emit.js +19 -28
- package/dist/engine/guardian/cli.js +150 -51
- package/dist/engine/guardian/coverage.js +1 -1
- package/dist/engine/guardian/diff-coverage/heuristic-tier.js +1 -1
- package/dist/engine/guardian/diff-coverage/orchestrator.js +2 -2
- package/dist/engine/guardian/pr-check.js +322 -33
- package/dist/engine/guardian/pr-comment.js +4 -3
- package/dist/engine/history/cli.js +210 -6
- package/dist/engine/history/ndjson-store.js +9 -5
- package/dist/engine/history/record.js +34 -5
- package/dist/engine/history/run-recorder.js +165 -0
- package/dist/engine/history/schema.js +25 -7
- package/dist/engine/history/store.js +9 -0
- package/dist/engine/mcp-server.js +35 -13
- package/dist/engine/skills-cli.js +249 -23
- package/dist/engine/util/ensure-ascii.js +37 -0
- package/dist/engine/workflow-cli.js +6 -6
- package/dist/gate-result.d.ts +11 -0
- package/dist/gate-result.js +18 -0
- package/dist/reporters/testtracker.d.ts +1 -1
- package/dist/reporters/testtracker.js +1 -1
- package/dist/uninstall.js +12 -5
- package/package.json +3 -2
package/dist/engine/cli.core.js
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Conventions follow `guardian/cli.ts` (see `cli-common.ts`): a
|
|
9
9
|
* {@link createCanaryCommand} factory wired to an injectable {@link MainDeps},
|
|
10
|
-
* `
|
|
11
|
-
* subcommand so usage errors exit 2 (typer/click) not commander's
|
|
10
|
+
* `CliExitError` for business exits, `normalizeUsageExit` on the program AND
|
|
11
|
+
* every subcommand so usage errors exit 2 (typer/click) not commander's 1, and
|
|
12
12
|
* an eager `-V/--version` global option + `_main` callback analog.
|
|
13
13
|
*
|
|
14
14
|
* Sub-app mounting note: `guardian`/`history`/`analyze` are mounted as FRESH
|
|
@@ -17,9 +17,9 @@
|
|
|
17
17
|
* mount into multiple `createCanaryCommand()` calls, e.g. across tests).
|
|
18
18
|
*/
|
|
19
19
|
import { Command, Option } from 'commander';
|
|
20
|
-
import {
|
|
20
|
+
import { CliExitError, normalizeUsageExit } from './cli-common.js';
|
|
21
21
|
import { createAnalyzeCommand } from './analysis/cli.js';
|
|
22
|
-
import {
|
|
22
|
+
import { doctorCmd, feedbackCmd, flakeCheckCmd, listFrameworksCmd, healTestCmd, initCmd, migrateCmd, overlayCmd, promoteCheckCmd, recommendFrameworkCmd, reviewTestCmd, runCmd, setupCmd, ticketUpdateCmd, uninstallCmd, upgradeCmd, vacuityCheckCmd, versionCmd, } from './cli-commands.js';
|
|
23
23
|
import { buildCompanyKnowledgeCommand } from './company-knowledge-cli.js';
|
|
24
24
|
import { createGuardianCommand } from './guardian/cli.js';
|
|
25
25
|
import { createHistoryCommand } from './history/cli.js';
|
|
@@ -38,18 +38,18 @@ export function createCanaryCommand(depsInit = {}) {
|
|
|
38
38
|
.hook('preAction', () => {
|
|
39
39
|
if (program.opts()['version']) {
|
|
40
40
|
versionCmd(deps);
|
|
41
|
-
throw new
|
|
41
|
+
throw new CliExitError(0);
|
|
42
42
|
}
|
|
43
43
|
})
|
|
44
44
|
.action(() => {
|
|
45
45
|
if (program.opts()['version']) {
|
|
46
46
|
versionCmd(deps);
|
|
47
|
-
throw new
|
|
47
|
+
throw new CliExitError(0);
|
|
48
48
|
}
|
|
49
49
|
// Python `typer.Typer(no_args_is_help=True)` prints help and exits 2 on a
|
|
50
50
|
// bare `canary` invocation (a usage exit), NOT 0.
|
|
51
51
|
program.outputHelp();
|
|
52
|
-
throw new
|
|
52
|
+
throw new CliExitError(2);
|
|
53
53
|
});
|
|
54
54
|
program
|
|
55
55
|
.command('recommend')
|
|
@@ -57,14 +57,14 @@ export function createCanaryCommand(depsInit = {}) {
|
|
|
57
57
|
.argument('<prompt>')
|
|
58
58
|
.option('--json', 'Output as JSON for tool integration.')
|
|
59
59
|
.action((prompt, opts) => {
|
|
60
|
-
|
|
60
|
+
recommendFrameworkCmd(prompt, opts, deps);
|
|
61
61
|
});
|
|
62
62
|
program
|
|
63
63
|
.command('frameworks')
|
|
64
64
|
.description('List the supported testing frameworks and how to run each.')
|
|
65
65
|
.option('--json', 'Dump the registry as JSON for tool integration.')
|
|
66
66
|
.action((opts) => {
|
|
67
|
-
|
|
67
|
+
listFrameworksCmd(opts, deps);
|
|
68
68
|
});
|
|
69
69
|
program
|
|
70
70
|
.command('feedback')
|
|
@@ -107,6 +107,7 @@ export function createCanaryCommand(depsInit = {}) {
|
|
|
107
107
|
.option('-o, --overlay <path>', '[deprecated: use --from] Path to an overlay repo whose .canary/skills/ are deployed.')
|
|
108
108
|
.option('--apply', 'Write files. Without this flag the command is a dry run.')
|
|
109
109
|
.option('--check', 'Freshness gate: report drift without writing.')
|
|
110
|
+
.option('--adoption-report', 'Report which adoption pieces (company.json, shape, overlay, skills, manifest, workflows) are present or missing in this repo. Writes nothing. Exits 0 fully adopted, 1 pieces missing, 3 nothing verifiable.')
|
|
110
111
|
.option('--force', 'Overwrite a .github/workflows/ file that differs from the overlay template. Without this flag a difference is only reported -- your CI is never rewritten behind your back.')
|
|
111
112
|
.option('--json', 'Emit the report as JSON.')
|
|
112
113
|
.action((opts) => {
|
|
@@ -131,6 +132,22 @@ export function createCanaryCommand(depsInit = {}) {
|
|
|
131
132
|
.action((path, opts) => {
|
|
132
133
|
flakeCheckCmd(path, opts, deps);
|
|
133
134
|
});
|
|
135
|
+
program
|
|
136
|
+
.command('promote-check')
|
|
137
|
+
.description('Decide whether a generated test may be promoted into the committed suite.')
|
|
138
|
+
.argument('<path>', 'The generated test file to judge.')
|
|
139
|
+
.option('--json', 'Emit the structured verdict as JSON.')
|
|
140
|
+
.action((path, opts) => {
|
|
141
|
+
promoteCheckCmd(path, opts, deps);
|
|
142
|
+
});
|
|
143
|
+
program
|
|
144
|
+
.command('vacuity-check')
|
|
145
|
+
.description('Find tests that pass without proving anything -- advisory, no LLM required.')
|
|
146
|
+
.argument('<path>', 'Test file or directory to scan.')
|
|
147
|
+
.option('--json', 'Output the verdict and its denominator as JSON.')
|
|
148
|
+
.action((path, opts) => {
|
|
149
|
+
vacuityCheckCmd(path, opts, deps);
|
|
150
|
+
});
|
|
134
151
|
program
|
|
135
152
|
.command('heal-test')
|
|
136
153
|
.description('Apply deterministic pattern fixes to a test file -- no LLM required.')
|
|
@@ -162,7 +179,7 @@ export function createCanaryCommand(depsInit = {}) {
|
|
|
162
179
|
.allowExcessArguments(true)
|
|
163
180
|
.argument('[args...]')
|
|
164
181
|
.action(() => {
|
|
165
|
-
|
|
182
|
+
overlayCmd(deps);
|
|
166
183
|
});
|
|
167
184
|
program
|
|
168
185
|
.command('doctor')
|
|
@@ -171,7 +188,16 @@ export function createCanaryCommand(depsInit = {}) {
|
|
|
171
188
|
.allowExcessArguments(true)
|
|
172
189
|
.argument('[args...]')
|
|
173
190
|
.action(() => {
|
|
174
|
-
|
|
191
|
+
doctorCmd(deps);
|
|
192
|
+
});
|
|
193
|
+
program
|
|
194
|
+
.command('uninstall')
|
|
195
|
+
.description('Remove installed Canary artifacts (requires the npm install of Canary).')
|
|
196
|
+
.allowUnknownOption(true)
|
|
197
|
+
.allowExcessArguments(true)
|
|
198
|
+
.argument('[args...]')
|
|
199
|
+
.action(() => {
|
|
200
|
+
uninstallCmd(deps);
|
|
175
201
|
});
|
|
176
202
|
program
|
|
177
203
|
.command('ticket-update')
|
package/dist/engine/cli.js
CHANGED
|
@@ -7,7 +7,7 @@ import { createRequire } from 'node:module';
|
|
|
7
7
|
import { CommanderError } from 'commander';
|
|
8
8
|
|
|
9
9
|
import { createCanaryCommand } from './cli.core.js';
|
|
10
|
-
import {
|
|
10
|
+
import { CliExitError } from './cli-common.js';
|
|
11
11
|
|
|
12
12
|
const require = createRequire(import.meta.url);
|
|
13
13
|
|
|
@@ -24,7 +24,7 @@ const program = createCanaryCommand({ pkgVersion: () => readVersion() });
|
|
|
24
24
|
try {
|
|
25
25
|
await program.parseAsync(process.argv.slice(2), { from: 'user' });
|
|
26
26
|
} catch (err) {
|
|
27
|
-
if (err instanceof
|
|
27
|
+
if (err instanceof CliExitError) process.exit(err.code);
|
|
28
28
|
if (err instanceof CommanderError) process.exit(err.exitCode);
|
|
29
29
|
console.error(err);
|
|
30
30
|
process.exit(1);
|
|
@@ -11,14 +11,14 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
|
11
11
|
import { join } from 'node:path';
|
|
12
12
|
import { Command, Option } from 'commander';
|
|
13
13
|
import pc from 'picocolors';
|
|
14
|
-
import {
|
|
14
|
+
import { CliExitError, jsonIndent2, normalizeUsageExit } from './cli-common.js';
|
|
15
15
|
import { CompanyKnowledge } from './core/company-knowledge.js';
|
|
16
16
|
import { CHECK, CROSS, WARN } from './main-deps.js';
|
|
17
17
|
function ckShowCmd(opts, deps) {
|
|
18
18
|
const ck = deps.loadCompanyKnowledge(opts.env ?? null);
|
|
19
19
|
if (ck.error && ck.isEmpty) {
|
|
20
20
|
deps.out(`${pc.red(CROSS)} ${ck.error}`);
|
|
21
|
-
throw new
|
|
21
|
+
throw new CliExitError(1);
|
|
22
22
|
}
|
|
23
23
|
if (opts.json) {
|
|
24
24
|
deps.out(jsonIndent2(ck.toDict()));
|
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The adoption report (#459, acceptance criterion 4).
|
|
3
|
+
*
|
|
4
|
+
* Answers one question for a consumer repo: **which pieces of a canary overlay
|
|
5
|
+
* adoption are actually in place here, and which are not?** The reported
|
|
6
|
+
* failure behind #459 is a repo that looks adopted -- overlay skills sitting in
|
|
7
|
+
* `.canary/skills/` -- while no workflow was ever installed, so the guardian
|
|
8
|
+
* never runs and every surface stays quiet. A partial adoption has to be
|
|
9
|
+
* *nameable* before it can be fixed.
|
|
10
|
+
*
|
|
11
|
+
* ## Why it lives here and not in `canary doctor`
|
|
12
|
+
*
|
|
13
|
+
* The issue asks for a "doctor-style" report, and `doctor` would be the natural
|
|
14
|
+
* home -- but `doctor` lives in the CommonJS npm shim (`npm/src/doctor.ts`),
|
|
15
|
+
* which structurally cannot import the ESM engine (see
|
|
16
|
+
* `npm/scripts/sync-gate-result.mjs` for the same boundary). Every piece this
|
|
17
|
+
* report inspects is an engine concept: shape resolution, `deploy_to` matching,
|
|
18
|
+
* the deploy manifest, `install_workflows`. Re-deriving shape detection in the
|
|
19
|
+
* shim to keep the report in `doctor` would be a second implementation of the
|
|
20
|
+
* thing most likely to drift. So the report is doctor-*style* (per-piece
|
|
21
|
+
* present/missing lines with a remedy under each gap) and ships on the command
|
|
22
|
+
* that owns the machinery: `canary migrate --adoption-report`.
|
|
23
|
+
*
|
|
24
|
+
* ## What it never does
|
|
25
|
+
*
|
|
26
|
+
* It writes nothing. The skill and workflow pieces are read from
|
|
27
|
+
* {@link HarnessMigrator.checkFreshness}, whose workflow phase already runs as
|
|
28
|
+
* a dry run, so the report is a pure observation of on-disk state.
|
|
29
|
+
*
|
|
30
|
+
* ## The ownership line, restated
|
|
31
|
+
*
|
|
32
|
+
* A consumer's `.github/workflows/` is theirs (#459 decision). A workflow that
|
|
33
|
+
* differs from the overlay template is therefore **present** -- adopted, then
|
|
34
|
+
* edited -- never a finding. Only a declared template with *no file at all* is
|
|
35
|
+
* missing. Reporting an edited workflow as a gap would nag about the single
|
|
36
|
+
* thing canary has no claim over, which is a worse failure than the partial
|
|
37
|
+
* adoption this report exists to surface.
|
|
38
|
+
*/
|
|
39
|
+
import { existsSync } from 'node:fs';
|
|
40
|
+
import { join } from 'node:path';
|
|
41
|
+
import { CompanyKnowledge } from './company-knowledge.js';
|
|
42
|
+
import { gateOutcome } from './gate-result.js';
|
|
43
|
+
import { DEPLOY_MANIFEST_NAME, } from './migrator.js';
|
|
44
|
+
const CHECK = '\u{2705}'; // white heavy check mark
|
|
45
|
+
const CROSS = '\u{274C}'; // cross mark
|
|
46
|
+
const WARN = '\u{26A0}'; // warning sign
|
|
47
|
+
const EMDASH = '\u{2014}'; // em dash
|
|
48
|
+
/**
|
|
49
|
+
* Workflow statuses that mean *a file exists at that path*.
|
|
50
|
+
*
|
|
51
|
+
* `outdated` and `conflict` included on purpose: both describe a workflow the
|
|
52
|
+
* repo HAS. The overlay having moved on, or the consumer having tuned their own
|
|
53
|
+
* CI, is a freshness question -- `migrate --check` already reports it -- not an
|
|
54
|
+
* adoption gap.
|
|
55
|
+
*/
|
|
56
|
+
const WORKFLOW_PRESENT = new Set([
|
|
57
|
+
'installed',
|
|
58
|
+
'updated',
|
|
59
|
+
'skipped',
|
|
60
|
+
'outdated',
|
|
61
|
+
'conflict',
|
|
62
|
+
]);
|
|
63
|
+
/** The report: a list of pieces, and the single decision that follows. */
|
|
64
|
+
export class AdoptionReport {
|
|
65
|
+
pieces;
|
|
66
|
+
constructor(pieces) {
|
|
67
|
+
this.pieces = pieces;
|
|
68
|
+
}
|
|
69
|
+
get present() {
|
|
70
|
+
return this.pieces.filter((p) => p.status === 'present');
|
|
71
|
+
}
|
|
72
|
+
get missing() {
|
|
73
|
+
return this.pieces.filter((p) => p.status === 'missing');
|
|
74
|
+
}
|
|
75
|
+
/** Pieces the report could not reach. Reported, never counted as verified. */
|
|
76
|
+
get unverifiable() {
|
|
77
|
+
return this.pieces.filter((p) => p.status === 'unknown');
|
|
78
|
+
}
|
|
79
|
+
/** The denominator: pieces that produced a real verdict. */
|
|
80
|
+
get checked() {
|
|
81
|
+
return this.present.length + this.missing.length;
|
|
82
|
+
}
|
|
83
|
+
/** True only when every piece was verified AND every piece is in place. */
|
|
84
|
+
get adopted() {
|
|
85
|
+
return (this.checked > 0 &&
|
|
86
|
+
this.missing.length === 0 &&
|
|
87
|
+
this.unverifiable.length === 0);
|
|
88
|
+
}
|
|
89
|
+
skipped() {
|
|
90
|
+
return this.unverifiable.map((p) => ({
|
|
91
|
+
name: p.id,
|
|
92
|
+
reason: p.skipReason ?? p.detail,
|
|
93
|
+
}));
|
|
94
|
+
}
|
|
95
|
+
/** A report that reached nothing has abstained, not passed (#508). */
|
|
96
|
+
get abstained() {
|
|
97
|
+
return gateOutcome({
|
|
98
|
+
checked: this.checked,
|
|
99
|
+
findings: this.missing,
|
|
100
|
+
skipped: this.skipped(),
|
|
101
|
+
}, 'gate').abstained;
|
|
102
|
+
}
|
|
103
|
+
/** 0 fully adopted, 1 pieces missing, 3 nothing verifiable (#508 D4). */
|
|
104
|
+
exit_code() {
|
|
105
|
+
return gateOutcome({
|
|
106
|
+
checked: this.checked,
|
|
107
|
+
findings: this.missing,
|
|
108
|
+
skipped: this.skipped(),
|
|
109
|
+
}, 'gate').exitCode;
|
|
110
|
+
}
|
|
111
|
+
to_dict() {
|
|
112
|
+
return {
|
|
113
|
+
schema_version: 1,
|
|
114
|
+
adopted: this.adopted,
|
|
115
|
+
checked: this.checked,
|
|
116
|
+
missing: this.missing.length,
|
|
117
|
+
unverifiable: this.unverifiable.length,
|
|
118
|
+
abstained: this.abstained,
|
|
119
|
+
exit_code: this.exit_code(),
|
|
120
|
+
pieces: this.pieces.map((p) => ({
|
|
121
|
+
id: p.id,
|
|
122
|
+
label: p.label,
|
|
123
|
+
status: p.status,
|
|
124
|
+
detail: p.detail,
|
|
125
|
+
...(p.remedy !== undefined ? { remedy: p.remedy } : {}),
|
|
126
|
+
})),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
to_markdown() {
|
|
130
|
+
const symbol = {
|
|
131
|
+
present: CHECK,
|
|
132
|
+
missing: CROSS,
|
|
133
|
+
unknown: WARN,
|
|
134
|
+
};
|
|
135
|
+
const lines = ['# Canary Adoption', ''];
|
|
136
|
+
for (const p of this.pieces) {
|
|
137
|
+
lines.push(`- ${symbol[p.status]} **${p.label}** ${EMDASH} ${p.detail}`);
|
|
138
|
+
if (p.remedy)
|
|
139
|
+
lines.push(` - fix: ${p.remedy}`);
|
|
140
|
+
}
|
|
141
|
+
lines.push('');
|
|
142
|
+
if (this.unverifiable.length > 0) {
|
|
143
|
+
lines.push(`${this.unverifiable.length} piece(s) were **not verified** ` +
|
|
144
|
+
`${EMDASH} that is not the same as being in place.`, '');
|
|
145
|
+
}
|
|
146
|
+
lines.push(gateOutcome({
|
|
147
|
+
checked: this.checked,
|
|
148
|
+
findings: this.missing,
|
|
149
|
+
skipped: this.skipped(),
|
|
150
|
+
}, 'gate', { noun: 'adoption piece(s)' }).summaryLine);
|
|
151
|
+
if (this.abstained) {
|
|
152
|
+
lines.push('', `Nothing was verified, so this is not a pass. Track an overlay ` +
|
|
153
|
+
'(`canary overlay add <source>`) and re-run, so there is something ' +
|
|
154
|
+
'to compare this repo against.');
|
|
155
|
+
}
|
|
156
|
+
return lines.join('\n');
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/** `.canary/company.json` -- the pointer file every other piece reads. */
|
|
160
|
+
function companyJsonPiece(input) {
|
|
161
|
+
return input.companyJsonPresent
|
|
162
|
+
? {
|
|
163
|
+
id: 'company_json',
|
|
164
|
+
label: 'Company knowledge',
|
|
165
|
+
status: 'present',
|
|
166
|
+
detail: '`.canary/company.json` is present',
|
|
167
|
+
}
|
|
168
|
+
: {
|
|
169
|
+
id: 'company_json',
|
|
170
|
+
label: 'Company knowledge',
|
|
171
|
+
status: 'missing',
|
|
172
|
+
detail: 'no `.canary/company.json` in this repo',
|
|
173
|
+
remedy: 'run `canary setup` to scaffold it',
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* A resolved test shape.
|
|
178
|
+
*
|
|
179
|
+
* Load-bearing rather than cosmetic: `deploy_to` matching and `<shape>:`-keyed
|
|
180
|
+
* workflow variants are both shape-keyed, so an unresolved shape silently
|
|
181
|
+
* deploys and installs nothing (#504).
|
|
182
|
+
*/
|
|
183
|
+
function shapePiece(input) {
|
|
184
|
+
return input.shapes.length > 0
|
|
185
|
+
? {
|
|
186
|
+
id: 'shape',
|
|
187
|
+
label: 'Test shape resolved',
|
|
188
|
+
status: 'present',
|
|
189
|
+
detail: input.shapes.map((s) => `\`${s}\``).join(', '),
|
|
190
|
+
}
|
|
191
|
+
: {
|
|
192
|
+
id: 'shape',
|
|
193
|
+
label: 'Test shape resolved',
|
|
194
|
+
status: 'missing',
|
|
195
|
+
detail: 'no shape was detected, so every shape-keyed skill and workflow ' +
|
|
196
|
+
'variant matches nothing',
|
|
197
|
+
remedy: 'set `canary_shape` in `.canary/company.json`, or pass ' +
|
|
198
|
+
'`--framework <name>` to resolve one',
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* The portable-path pointers a generated workflow interpolates.
|
|
203
|
+
*
|
|
204
|
+
* Undeclared is `unknown`, not `missing`: a template with no pointer falls back
|
|
205
|
+
* to its own default, and whether that default fits this repo is not knowable
|
|
206
|
+
* from the repo alone. Calling it a gap would raise a finding on every
|
|
207
|
+
* lcov-default repo that is working perfectly.
|
|
208
|
+
*/
|
|
209
|
+
function pathPointersPiece(input) {
|
|
210
|
+
const declared = [];
|
|
211
|
+
if (input.coverageReportPath)
|
|
212
|
+
declared.push(`coverage_report_path=\`${input.coverageReportPath}\``);
|
|
213
|
+
if (input.sutControllersPath)
|
|
214
|
+
declared.push(`sut_controllers_path=\`${input.sutControllersPath}\``);
|
|
215
|
+
if (declared.length > 0) {
|
|
216
|
+
return {
|
|
217
|
+
id: 'path_pointers',
|
|
218
|
+
label: 'Portable path pointers',
|
|
219
|
+
status: 'present',
|
|
220
|
+
detail: declared.join(', '),
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
return {
|
|
224
|
+
id: 'path_pointers',
|
|
225
|
+
label: 'Portable path pointers',
|
|
226
|
+
status: 'unknown',
|
|
227
|
+
detail: 'neither `coverage_report_path` nor `sut_controllers_path` is declared, ' +
|
|
228
|
+
'so a workflow template that reads one uses its own default ' +
|
|
229
|
+
`${EMDASH} whether that default fits this repo cannot be told from here`,
|
|
230
|
+
remedy: 'declare the repo-relative paths in `.canary/company.json` if the ' +
|
|
231
|
+
"installed workflow's defaults are wrong",
|
|
232
|
+
skipReason: 'none declared',
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
/** An overlay to adopt from. Without one, nothing overlay-derived is knowable. */
|
|
236
|
+
function overlayPiece(input) {
|
|
237
|
+
return input.overlayPath !== null
|
|
238
|
+
? {
|
|
239
|
+
id: 'overlay',
|
|
240
|
+
label: 'Overlay resolved',
|
|
241
|
+
status: 'present',
|
|
242
|
+
detail: `\`${input.overlayPath}\``,
|
|
243
|
+
}
|
|
244
|
+
: {
|
|
245
|
+
id: 'overlay',
|
|
246
|
+
label: 'Overlay resolved',
|
|
247
|
+
status: 'missing',
|
|
248
|
+
detail: 'no overlay is tracked or passed, so nothing was deployed here',
|
|
249
|
+
remedy: 'track one with `canary overlay add <source>`, or pass ' +
|
|
250
|
+
'`--from <overlay>`',
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
/** Overlay skills deployed under `.canary/skills/`. */
|
|
254
|
+
function skillsPiece(input) {
|
|
255
|
+
const label = 'Overlay skills deployed';
|
|
256
|
+
const freshness = input.freshness;
|
|
257
|
+
if (freshness === null) {
|
|
258
|
+
return {
|
|
259
|
+
id: 'skills',
|
|
260
|
+
label,
|
|
261
|
+
status: 'unknown',
|
|
262
|
+
detail: 'no overlay to compare against, so deployment was not checked',
|
|
263
|
+
remedy: 'resolve an overlay (see above) and re-run',
|
|
264
|
+
skipReason: 'no overlay',
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
if (freshness.results.length === 0) {
|
|
268
|
+
return {
|
|
269
|
+
id: 'skills',
|
|
270
|
+
label,
|
|
271
|
+
status: 'unknown',
|
|
272
|
+
detail: "zero overlay skills matched this repo's shape, so nothing was " +
|
|
273
|
+
'verified',
|
|
274
|
+
remedy: "check the overlay's `deploy_to` lists against the resolved shape",
|
|
275
|
+
skipReason: 'no skill matched the shape',
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
const absent = freshness.results.filter((r) => r.status === 'missing');
|
|
279
|
+
if (absent.length === 0) {
|
|
280
|
+
return {
|
|
281
|
+
id: 'skills',
|
|
282
|
+
label,
|
|
283
|
+
status: 'present',
|
|
284
|
+
detail: `${freshness.results.length} matching skill(s) deployed`,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
return {
|
|
288
|
+
id: 'skills',
|
|
289
|
+
label,
|
|
290
|
+
status: 'missing',
|
|
291
|
+
detail: `${absent.length} of ${freshness.results.length} matching skill(s) ` +
|
|
292
|
+
`not deployed: ${absent.map((r) => `\`${r.skill_name}\``).join(', ')}`,
|
|
293
|
+
remedy: 'run `canary migrate --apply` to deploy them',
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* The deploy manifest.
|
|
298
|
+
*
|
|
299
|
+
* Its own piece rather than a footnote on the skills line: without it the
|
|
300
|
+
* freshness gate cannot tell "the overlay moved ahead" from "the consumer
|
|
301
|
+
* edited this", so drift detection is structurally unreachable however many
|
|
302
|
+
* skills are deployed.
|
|
303
|
+
*/
|
|
304
|
+
function manifestPiece(input) {
|
|
305
|
+
return input.manifestPresent
|
|
306
|
+
? {
|
|
307
|
+
id: 'manifest',
|
|
308
|
+
label: 'Deploy manifest',
|
|
309
|
+
status: 'present',
|
|
310
|
+
detail: `\`.canary/skills/${DEPLOY_MANIFEST_NAME}\` records provenance`,
|
|
311
|
+
}
|
|
312
|
+
: {
|
|
313
|
+
id: 'manifest',
|
|
314
|
+
label: 'Deploy manifest',
|
|
315
|
+
status: 'missing',
|
|
316
|
+
detail: `no \`.canary/skills/${DEPLOY_MANIFEST_NAME}\`, so drift cannot be ` +
|
|
317
|
+
'told apart from a local edit',
|
|
318
|
+
remedy: 'run `canary migrate --apply` to record it',
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
/** Declared workflow templates actually sitting in `.github/workflows/`. */
|
|
322
|
+
function workflowsPiece(input) {
|
|
323
|
+
const label = 'Overlay workflows installed';
|
|
324
|
+
const freshness = input.freshness;
|
|
325
|
+
if (freshness === null) {
|
|
326
|
+
return {
|
|
327
|
+
id: 'workflows',
|
|
328
|
+
label,
|
|
329
|
+
status: 'unknown',
|
|
330
|
+
detail: 'no overlay to compare against, so `.github/workflows/` was not ' +
|
|
331
|
+
'checked',
|
|
332
|
+
remedy: 'resolve an overlay (see above) and re-run',
|
|
333
|
+
skipReason: 'no overlay',
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
if (freshness.workflows.length === 0) {
|
|
337
|
+
return {
|
|
338
|
+
id: 'workflows',
|
|
339
|
+
label,
|
|
340
|
+
status: 'unknown',
|
|
341
|
+
detail: 'no matching overlay skill declares an `install_workflows` template, ' +
|
|
342
|
+
'so there is nothing to install into `.github/workflows/`',
|
|
343
|
+
remedy: 'if a workflow is expected, check the declaring skill deploys for ' +
|
|
344
|
+
'this shape',
|
|
345
|
+
skipReason: 'no template declared',
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
const absent = freshness.workflows.filter((w) => !WORKFLOW_PRESENT.has(w.status));
|
|
349
|
+
if (absent.length === 0) {
|
|
350
|
+
return {
|
|
351
|
+
id: 'workflows',
|
|
352
|
+
label,
|
|
353
|
+
status: 'present',
|
|
354
|
+
detail: `${freshness.workflows.length} declared workflow(s) in \`.github/workflows/\``,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
return {
|
|
358
|
+
id: 'workflows',
|
|
359
|
+
label,
|
|
360
|
+
status: 'missing',
|
|
361
|
+
detail: absent
|
|
362
|
+
.map((w) => `\`${w.workflow}\` (${w.status}) ${EMDASH} ${w.detail}`)
|
|
363
|
+
.join('; '),
|
|
364
|
+
remedy: 'run `canary migrate --apply` to install the declared workflow(s)',
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Derive the report from gathered facts.
|
|
369
|
+
*
|
|
370
|
+
* Piece order is the adoption order: local config first, then the overlay, then
|
|
371
|
+
* what the overlay put here. A reader hitting the first `missing` line has
|
|
372
|
+
* found the step to fix, because nothing below it can work without it.
|
|
373
|
+
*/
|
|
374
|
+
export function buildAdoptionReport(input) {
|
|
375
|
+
return new AdoptionReport([
|
|
376
|
+
companyJsonPiece(input),
|
|
377
|
+
shapePiece(input),
|
|
378
|
+
pathPointersPiece(input),
|
|
379
|
+
overlayPiece(input),
|
|
380
|
+
skillsPiece(input),
|
|
381
|
+
manifestPiece(input),
|
|
382
|
+
workflowsPiece(input),
|
|
383
|
+
]);
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Read the target repo and build its adoption report. Writes nothing.
|
|
387
|
+
*
|
|
388
|
+
* `home` is the company-knowledge cascade's home layer (a test seam, matching
|
|
389
|
+
* `CompanyKnowledge.load`); the *presence* check deliberately looks only at the
|
|
390
|
+
* project-local file, because an org-wide `~/.canary/company.json` is not
|
|
391
|
+
* something this repo has adopted.
|
|
392
|
+
*/
|
|
393
|
+
export function gatherAdoptionReport(migrator, projectRoot, overlayPath, home) {
|
|
394
|
+
const ctx = migrator.detect(projectRoot);
|
|
395
|
+
const ck = CompanyKnowledge.load(projectRoot, null, home);
|
|
396
|
+
return buildAdoptionReport({
|
|
397
|
+
shapes: ctx.shapes,
|
|
398
|
+
overlayPath,
|
|
399
|
+
freshness: overlayPath === null
|
|
400
|
+
? null
|
|
401
|
+
: migrator.checkFreshness(projectRoot, { overlayPath }),
|
|
402
|
+
companyJsonPresent: existsSync(join(projectRoot, '.canary', 'company.json')),
|
|
403
|
+
manifestPresent: existsSync(join(projectRoot, '.canary', 'skills', DEPLOY_MANIFEST_NAME)),
|
|
404
|
+
coverageReportPath: ck.coverage_report_path,
|
|
405
|
+
sutControllersPath: ck.sut_controllers_path,
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
//# sourceMappingURL=adoption.js.map
|
|
@@ -9,7 +9,7 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
9
9
|
import { join } from 'node:path';
|
|
10
10
|
import { globFiles, readTextOrNull } from './fs-glob.js';
|
|
11
11
|
// (config_file, framework, shape, confidence)
|
|
12
|
-
export const
|
|
12
|
+
export const CONFIG_PROBES = [
|
|
13
13
|
['playwright.config.ts', 'playwright', 'e2e_ui', 'config'],
|
|
14
14
|
['playwright.config.js', 'playwright', 'e2e_ui', 'config'],
|
|
15
15
|
['cypress.config.ts', 'playwright', 'e2e_ui', 'config'],
|
|
@@ -72,7 +72,7 @@ const _PW_UI_FIXTURE_RE = /async\s*\(\s*\{[^}]*\b(?:page|browser)\b/;
|
|
|
72
72
|
* Return 'api' when no playwright spec file uses page/browser fixtures, else
|
|
73
73
|
* 'e2e_ui' (the default when any UI signal is found or no spec files exist).
|
|
74
74
|
*/
|
|
75
|
-
export function
|
|
75
|
+
export function inferPlaywrightTestType(root) {
|
|
76
76
|
const specGlobs = [
|
|
77
77
|
'tests/**/*.spec.ts',
|
|
78
78
|
'tests/**/*.spec.js',
|
|
@@ -100,11 +100,11 @@ export function inferPlaywrightShape(root) {
|
|
|
100
100
|
}
|
|
101
101
|
/** Tier 1 -- a dedicated config file (highest confidence). */
|
|
102
102
|
function probeConfig(root) {
|
|
103
|
-
for (const [filename, framework, shape, confidence] of
|
|
103
|
+
for (const [filename, framework, shape, confidence] of CONFIG_PROBES) {
|
|
104
104
|
if (existsSync(join(root, filename))) {
|
|
105
105
|
// For playwright config files, distinguish API vs UI suites.
|
|
106
106
|
if (framework === 'playwright' && shape === 'e2e_ui') {
|
|
107
|
-
const inferred =
|
|
107
|
+
const inferred = inferPlaywrightTestType(root);
|
|
108
108
|
if (inferred !== shape)
|
|
109
109
|
return [framework, inferred, filename, 'content'];
|
|
110
110
|
}
|
|
@@ -193,10 +193,10 @@ function probeLanguage(config) {
|
|
|
193
193
|
* (#504 part 1, spec test #8).
|
|
194
194
|
*
|
|
195
195
|
* Note the tier list and the returned `confidence` are not the same axis: the
|
|
196
|
-
* config tier returns confidence `content` when `
|
|
197
|
-
* e2e_ui to api, because the refinement read file contents to decide.
|
|
196
|
+
* config tier returns confidence `content` when `inferPlaywrightTestType`
|
|
197
|
+
* refines e2e_ui to api, because the refinement read file contents to decide.
|
|
198
198
|
*/
|
|
199
|
-
export function
|
|
199
|
+
export function probeFramework(dir, config, tiers) {
|
|
200
200
|
const on = (t) => tiers.includes(t);
|
|
201
201
|
if (on('config')) {
|
|
202
202
|
const hit = probeConfig(dir);
|
|
@@ -93,7 +93,7 @@ function subDirs(dir) {
|
|
|
93
93
|
* silently suppress a scaffold the user needs -- and a `**` glob over a real
|
|
94
94
|
* monorepo would otherwise walk every installed package on disk.
|
|
95
95
|
*/
|
|
96
|
-
export const
|
|
96
|
+
export const WORKSPACE_SKIP_DIRS = new Set([
|
|
97
97
|
'node_modules',
|
|
98
98
|
'.git',
|
|
99
99
|
'.venv',
|
|
@@ -117,7 +117,7 @@ export const _WORKSPACE_SKIP_DIRS = new Set([
|
|
|
117
117
|
export function globDirs(root, pattern) {
|
|
118
118
|
const segments = pattern.split('/').filter((s) => s !== '');
|
|
119
119
|
const out = [];
|
|
120
|
-
const walkable = (dir) => subDirs(dir).filter((d) => !
|
|
120
|
+
const walkable = (dir) => subDirs(dir).filter((d) => !WORKSPACE_SKIP_DIRS.has(basename(d)));
|
|
121
121
|
const visit = (dir, si) => {
|
|
122
122
|
if (si === segments.length) {
|
|
123
123
|
if (dir !== root)
|
|
@@ -25,6 +25,23 @@ const EMDASH = '\u{2014}'; // em dash
|
|
|
25
25
|
// C0 controls (incl. \n, ESC) and DEL: a skip name must never be able to
|
|
26
26
|
// forge output lines or smuggle ANSI sequences into the summary.
|
|
27
27
|
const CONTROL_CHARS = /[\u0000-\u001F\u007F]/g;
|
|
28
|
+
// A libuv/POSIX errno code (`ENOENT`, `EACCES`, `EISDIR`), as opposed to a Node
|
|
29
|
+
// programmer-error code (`ERR_INVALID_ARG_TYPE`).
|
|
30
|
+
const ERRNO = /^E[A-Z0-9]+$/;
|
|
31
|
+
/**
|
|
32
|
+
* The errno code `e` carries, or `null` when it carries none.
|
|
33
|
+
*
|
|
34
|
+
* Load-bearing wherever a read failure is absorbed into a {@link SkipEntry}:
|
|
35
|
+
* only "the filesystem said no" may become a skip. Node PROGRAMMER errors carry
|
|
36
|
+
* a string `code` too, so keying off `typeof code === 'string'` reports a
|
|
37
|
+
* genuine defect inside the scanner as a tidy abstention with a misleading
|
|
38
|
+
* reason -- which is how a scanner learns to go quiet. Anything this returns
|
|
39
|
+
* `null` for must still throw.
|
|
40
|
+
*/
|
|
41
|
+
export function errnoCode(e) {
|
|
42
|
+
const code = e?.code;
|
|
43
|
+
return typeof code === 'string' && ERRNO.test(code) ? code : null;
|
|
44
|
+
}
|
|
28
45
|
/**
|
|
29
46
|
* D7: skipped entries render in EVERY summary line.
|
|
30
47
|
*
|