@event4u/agent-config 9.6.0 → 9.8.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/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +207 -942
- package/README.md +21 -21
- package/dist/agent-src/README.md +2 -2
- package/dist/agent-src/commands/commit/in-chunks.md +8 -0
- package/dist/agent-src/commands/commit.md +17 -0
- package/dist/agent-src/commands/pr/create.md +4 -0
- package/dist/agent-src/contexts/override-system.md +75 -5
- package/dist/agent-src/rules/context-hygiene.md +2 -0
- package/dist/agent-src/rules/doc-screenshot-hygiene.md +97 -0
- package/dist/agent-src/rules/domain-safety-pii.md +1 -0
- package/dist/agent-src/rules/downstream-changes.md +31 -1
- package/dist/agent-src/rules/external-code-graph-interop.md +13 -3
- package/dist/agent-src/rules/framework-neutrality-in-generic-skills.md +2 -0
- package/dist/agent-src/rules/git-history-discipline.md +2 -0
- package/dist/agent-src/rules/language-and-tone.md +2 -0
- package/dist/agent-src/rules/lethal-trifecta-guard.md +3 -0
- package/dist/agent-src/rules/media-governance-routing.md +2 -0
- package/dist/agent-src/rules/minimal-safe-diff.md +2 -0
- package/dist/agent-src/rules/no-roadmap-references.md +3 -0
- package/dist/agent-src/rules/non-destructive-by-default.md +20 -0
- package/dist/agent-src/rules/onboarding-gate.md +2 -0
- package/dist/agent-src/rules/output-discipline.md +2 -0
- package/dist/agent-src/rules/persona-governance.md +2 -0
- package/dist/agent-src/rules/preservation-guard.md +3 -0
- package/dist/agent-src/rules/roadmap-progress-sync.md +2 -0
- package/dist/agent-src/rules/secret-vcs-guard.md +106 -0
- package/dist/agent-src/rules/security-sensitive-stop.md +1 -1
- package/dist/agent-src/rules/skill-quality.md +2 -0
- package/dist/agent-src/rules/source-confidentiality.md +2 -0
- package/dist/agent-src/rules/source-of-truth.md +2 -0
- package/dist/agent-src/rules/telegraph-speak.md +2 -0
- package/dist/agent-src/rules/token-optimizer-maintenance.md +2 -0
- package/dist/agent-src/rules/tool-safety.md +1 -0
- package/dist/agent-src/rules/verify-before-complete.md +2 -0
- package/dist/agent-src/skills/adr-create/SKILL.md +17 -1
- package/dist/agent-src/skills/agent-docs-writing/SKILL.md +34 -16
- package/dist/agent-src/skills/agents-md-thin-root/SKILL.md +9 -9
- package/dist/agent-src/skills/code-intelligence/SKILL.md +94 -0
- package/dist/agent-src/skills/gated-reach/SKILL.md +185 -0
- package/dist/agent-src/skills/gated-reach/evals/triggers.json +72 -0
- package/dist/agent-src/skills/image-analyser/SKILL.md +14 -12
- package/dist/agent-src/skills/image-editing/SKILL.md +1 -0
- package/dist/agent-src/skills/readme-writing/SKILL.md +6 -1
- package/dist/agent-src/skills/screenshot-hygiene/SKILL.md +171 -0
- package/dist/agent-src/skills/secrets-management/SKILL.md +44 -1
- package/dist/agent-src/skills/subagent-orchestration/prompts/adversarial-verification-council.md +29 -0
- package/dist/agent-src/skills/subagent-orchestration/prompts/do-and-judge-two-stage.md +29 -0
- package/dist/agent-src/skills/subagent-orchestration/prompts/do-and-judge.md +29 -0
- package/dist/agent-src/skills/subagent-orchestration/prompts/do-competitively.md +29 -0
- package/dist/agent-src/skills/subagent-orchestration/prompts/do-in-parallel.md +29 -0
- package/dist/agent-src/skills/subagent-orchestration/prompts/do-in-steps.md +29 -0
- package/dist/agent-src/skills/subagent-orchestration/prompts/do-in-worktrees.md +29 -0
- package/dist/agent-src/skills/subagent-orchestration/prompts/judge-with-debate.md +29 -0
- package/dist/agent-src/skills/verify-repair-loop/SKILL.md +4 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/github-workflows/doc-impact.yml +101 -0
- package/dist/agent-src/templates/github-workflows/secret-scan.yml +65 -0
- package/dist/cli/configRoot.js +112 -0
- package/dist/cli/configRoot.js.map +1 -0
- package/dist/cli/configRoot.test.js +97 -0
- package/dist/cli/configRoot.test.js.map +1 -0
- package/dist/cli/help.js +4 -1
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/main.js +19 -2
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/registry.js +1 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +164 -47
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +4 -4
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +13 -8
- package/dist/discovery/trust-report.md +11 -11
- package/dist/discovery/workspaces.json +11 -6
- package/dist/install/install.mjs +16 -0
- package/dist/mcp/awesome-mcp-servers.row.md +1 -1
- package/dist/mcp/mcp-cloudflare-catalogue.json +1 -1
- package/dist/mcp/registry-manifest.json +4 -4
- package/dist/router.json +1 -1
- package/dist/server/app.js +10 -0
- package/dist/server/app.js.map +1 -1
- package/dist/server/routes/ping.js +18 -0
- package/dist/server/routes/ping.js.map +1 -1
- package/dist/server/schemas/settings.js +8 -0
- package/dist/server/schemas/settings.js.map +1 -1
- package/dist/server/serverInfo.js +6 -2
- package/dist/server/serverInfo.js.map +1 -1
- package/dist/server/token.js +11 -4
- package/dist/server/token.js.map +1 -1
- package/dist/server/writeRoot.js +15 -1
- package/dist/server/writeRoot.js.map +1 -1
- package/dist/shared/capabilities.js +33 -0
- package/dist/shared/capabilities.js.map +1 -0
- package/dist/shared/capabilities.test.js +39 -0
- package/dist/shared/capabilities.test.js.map +1 -0
- package/dist/ui/assets/{index-BiEZiZ31.js → index-CrmZ34Rg.js} +16 -16
- package/dist/ui/assets/index-CrmZ34Rg.js.map +1 -0
- package/dist/ui/index.html +14 -5
- package/docs/CLAIMS.md +64 -38
- package/docs/architecture.md +2 -2
- package/docs/archive/CHANGELOG-6.0.0-overview.md +953 -0
- package/docs/benchmark.md +170 -9
- package/docs/catalog.md +8 -3
- package/docs/comparison.yaml +44 -2
- package/docs/contracts/local-server-ports.md +53 -0
- package/docs/contracts/no-runtime-boundary.md +1 -1
- package/docs/contracts/subagent-boundary.md +15 -0
- package/docs/customization.md +14 -3
- package/docs/decisions/ADR-016-installer-architecture.md +2 -0
- package/docs/decisions/ADR-088-no-external-runtime-federation.md +1 -1
- package/docs/decisions/ADR-094-agent-memory-layer-removal.md +1 -1
- package/docs/decisions/ADR-109-subagent-v1-contract.md +2 -0
- package/docs/decisions/ADR-110-discipline-profile-resolution-locus.md +2 -0
- package/docs/decisions/ADR-113-ci-native-release-label-trigger.md +11 -3
- package/docs/decisions/ADR-122-adversarial-verification-council.md +30 -0
- package/docs/decisions/ADR-123-runtime-security-scope-and-spawn-hardening.md +31 -0
- package/docs/decisions/ADR-124-embedded-engine-doctrine.md +255 -0
- package/docs/decisions/ADR-125-doc-screenshot-anonymization.md +131 -0
- package/docs/decisions/ADR-126-internet-reach-operator-tooling.md +254 -0
- package/docs/decisions/ADR-127-enforcement-claims-must-resolve.md +150 -0
- package/docs/decisions/ADR-128-executable-evidence-and-measured-escalation.md +201 -0
- package/docs/decisions/INDEX.md +21 -0
- package/docs/decisions/engine-reclassification-2026-07.md +104 -0
- package/docs/distribution/awesome-list-submission.md +1 -1
- package/docs/distribution/registries.md +36 -27
- package/docs/enforcement-by-host.md +7 -0
- package/docs/featured-skills.md +2 -2
- package/docs/getting-started-by-role.md +1 -1
- package/docs/getting-started.md +1 -1
- package/docs/governance-advantage.md +1 -1
- package/docs/guides/gated-platform-reads.md +221 -0
- package/docs/positioning-evidence.md +1 -1
- package/docs/proof.md +92 -16
- package/docs/spawn-site-policy.md +72 -0
- package/docs/threat-model.md +2 -1
- package/package.json +6 -3
- package/src/agent-src/README.md +2 -2
- package/src/config/agent-settings.template.yml +43 -0
- package/src/config/reach-channels.yml +217 -0
- package/src/config/reach-prescriptions-intake.yml +200 -0
- package/src/scripts/_dispatch.bash +15 -0
- package/src/scripts/_lib/adversarial_bench_score.ts +106 -0
- package/src/scripts/_lib/exec_evidence.ts +251 -0
- package/src/scripts/_lib/fixtures/secret-detector/negatives.txt +10 -0
- package/src/scripts/_lib/fixtures/secret-detector/positives.txt +11 -0
- package/src/scripts/_lib/kernel_rules.ts +46 -0
- package/src/scripts/_lib/reddit_thread_parse.ts +598 -0
- package/src/scripts/_lib/risk_paths.ts +160 -0
- package/src/scripts/_lib/secret_detector.test.ts +103 -0
- package/src/scripts/_lib/secret_detector.ts +319 -0
- package/src/scripts/_lib/spawn_env.ts +24 -4
- package/src/scripts/_lib/tool_probe.ts +359 -0
- package/src/scripts/adr/regenerate_index.ts +81 -6
- package/src/scripts/audit_adr_coverage.ts +33 -0
- package/src/scripts/bench_adversarial_council.ts +292 -0
- package/src/scripts/build_proof.ts +162 -0
- package/src/scripts/check_adr_frontmatter.ts +195 -0
- package/src/scripts/check_artefact_count_messaging.ts +4 -0
- package/src/scripts/check_backstop_debt.ts +200 -0
- package/src/scripts/check_claims.ts +188 -8
- package/src/scripts/check_comparison.ts +12 -0
- package/src/scripts/check_enforcement_coverage.ts +592 -0
- package/src/scripts/check_kernel_rule_bundle.ts +16 -12
- package/src/scripts/check_no_new_legacy_path.ts +2 -0
- package/src/scripts/check_reach_channels.ts +407 -0
- package/src/scripts/check_reach_staleness.ts +364 -0
- package/src/scripts/check_secret_leak.test.ts +47 -0
- package/src/scripts/check_secret_leak.ts +290 -0
- package/src/scripts/check_source_pointer_freshness.ts +165 -0
- package/src/scripts/code_graph/build.ts +0 -0
- package/src/scripts/code_graph/cli.ts +207 -0
- package/src/scripts/code_graph/detect.ts +134 -0
- package/src/scripts/code_graph/extract.ts +386 -0
- package/src/scripts/code_graph/loader.ts +123 -0
- package/src/scripts/code_graph/query.ts +176 -0
- package/src/scripts/code_graph/sanitize.ts +30 -0
- package/src/scripts/code_graph/types.ts +83 -0
- package/src/scripts/code_graph/validate.ts +63 -0
- package/src/scripts/external_sources_denylist.json +3 -1
- package/src/scripts/generate_subagent_floor.ts +194 -0
- package/src/scripts/hook_manifest.yaml +13 -3
- package/src/scripts/hooks/code_graph_nudge_hook.ts +184 -0
- package/src/scripts/hooks/dispatch_hook.ts +5 -1
- package/src/scripts/iron_law_sha.ts +3 -12
- package/src/scripts/lint_override_kernel_guard.ts +219 -0
- package/src/scripts/lint_readme_serial_comma.ts +169 -0
- package/src/scripts/measure_rule_budget.ts +3 -12
- package/src/scripts/postinstall_gui.ts +129 -0
- package/src/scripts/reach_doctor.ts +1651 -0
- package/src/scripts/release.ts +24 -3
- package/src/scripts/risk_escalation_shadow.ts +160 -0
- package/src/scripts/schemas/agent-settings.schema.json +23 -0
- package/src/scripts/schemas/reach-channels.schema.json +134 -0
- package/src/scripts/schemas/reach-doctor-payload.schema.json +220 -0
- package/src/scripts/schemas/rule.schema.json +10 -0
- package/src/scripts/skill_linter.ts +55 -0
- package/src/scripts/validate_reach_prescriptions.ts +751 -0
- package/src/server/schemas/settings.ts +16 -0
- package/src/shared/capabilities.test.ts +43 -0
- package/src/shared/capabilities.ts +75 -0
- package/src/templates/minimal/agents-overrides-readme.md +20 -0
- package/dist/ui/assets/index-BiEZiZ31.js.map +0 -1
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* CI secret-leak gate — the non-bypassable second layer behind the
|
|
4
|
+
* `secret-vcs-guard` behavioral rule.
|
|
5
|
+
*
|
|
6
|
+
* Runs the shared `secret_detector` library (regex rule pack + Shannon entropy +
|
|
7
|
+
* keyword context) over the repository's tracked working tree (or a supplied
|
|
8
|
+
* path set), and FAILS (exit 1) on any `high`-confidence finding that is not
|
|
9
|
+
* suppressed. This is the deterministic enforcing net: a PR cannot merge red,
|
|
10
|
+
* unlike a client-side commit hook (which fires only on agent-initiated bash and
|
|
11
|
+
* is `--no-verify`/GUI/fresh-clone bypassable) — see
|
|
12
|
+
* `src/rules/secret-vcs-guard.md` and `skill:secrets-management` for the
|
|
13
|
+
* layering rationale.
|
|
14
|
+
*
|
|
15
|
+
* Suppression (audited, narrow — never a global mute):
|
|
16
|
+
* - inline `# secret-allow` / `// secret-allow` / `<!-- secret-allow -->` on a
|
|
17
|
+
* line (handled inside `secret_detector`);
|
|
18
|
+
* - a repo-root `.secret-allow` file: one entry per line, `path` or
|
|
19
|
+
* `path:line`, `#` comments allowed. Each entry SHOULD carry a one-line
|
|
20
|
+
* justification comment so it is reviewable in the diff.
|
|
21
|
+
*
|
|
22
|
+
* Default excludes (fixtures/tests/examples legitimately hold fake secrets;
|
|
23
|
+
* generated + vendored trees are not source): node_modules, dist, dist-*,
|
|
24
|
+
* .git, .claude, **\/fixtures\/**, *.test.*, *.spec.*, *.example, *.sample,
|
|
25
|
+
* .env.example, lockfiles, *.min.*.
|
|
26
|
+
*
|
|
27
|
+
* Usage:
|
|
28
|
+
* ./scripts-run src/scripts/check_secret_leak [paths...] [--all] [--base <ref>] [--json]
|
|
29
|
+
* default → files changed vs origin/main + untracked (shift-left); --all →
|
|
30
|
+
* whole tracked tree; paths → exactly those (excludes skipped).
|
|
31
|
+
*
|
|
32
|
+
* Exit codes: 0 clean · 1 high-confidence leak found · 2 usage/env error.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import { spawnSync } from 'node:child_process';
|
|
36
|
+
import * as fs from 'node:fs';
|
|
37
|
+
import * as path from 'node:path';
|
|
38
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
39
|
+
|
|
40
|
+
import { scanText, type SecretFinding } from './_lib/secret_detector.js';
|
|
41
|
+
|
|
42
|
+
const _HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
43
|
+
// repo root: src/scripts/check_secret_leak.ts → parents[2].
|
|
44
|
+
export const REPO_ROOT = path.resolve(_HERE, '..', '..');
|
|
45
|
+
|
|
46
|
+
const DEFAULT_EXCLUDE: readonly RegExp[] = [
|
|
47
|
+
/(^|\/)node_modules(\/|$)/,
|
|
48
|
+
/(^|\/)dist(\/|$)/,
|
|
49
|
+
/(^|\/)dist-[^/]+(\/|$)/,
|
|
50
|
+
/(^|\/)\.git(\/|$)/,
|
|
51
|
+
/(^|\/)\.claude(\/|$)/,
|
|
52
|
+
/(^|\/)fixtures(\/|$)/,
|
|
53
|
+
/(^|\/)tests(\/|$)/, // test + golden + parity data legitimately holds secret-shaped strings
|
|
54
|
+
/(^|\/)__[a-z_]+__(\/|$)/, // snapshot dirs (e.g. __parity_snapshots__)
|
|
55
|
+
/\.test\.[a-z]+$/,
|
|
56
|
+
/\.spec\.[a-z]+$/,
|
|
57
|
+
/\.example$/,
|
|
58
|
+
/\.sample$/,
|
|
59
|
+
/(^|\/)\.env\.example$/,
|
|
60
|
+
/(^|\/)(package-lock\.json|pnpm-lock\.yaml|yarn\.lock|composer\.lock)$/,
|
|
61
|
+
/\.min\.[a-z]+$/,
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
export interface LeakHit extends SecretFinding {
|
|
65
|
+
file: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface AllowEntry {
|
|
69
|
+
file: string;
|
|
70
|
+
line: number | null; // null = whole-file allow
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Parse `.secret-allow` (repo-root) into path[:line] entries. */
|
|
74
|
+
export function readAllowFile(root: string): AllowEntry[] {
|
|
75
|
+
const p = path.join(root, '.secret-allow');
|
|
76
|
+
if (!fs.existsSync(p)) {
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
const out: AllowEntry[] = [];
|
|
80
|
+
for (const raw of fs.readFileSync(p, 'utf-8').split(/\r?\n/)) {
|
|
81
|
+
const line = raw.replace(/#.*$/, '').trim();
|
|
82
|
+
if (line === '') {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const m = /^(.*?)(?::(\d+))?$/.exec(line);
|
|
86
|
+
if (m) {
|
|
87
|
+
out.push({ file: m[1] as string, line: m[2] ? parseInt(m[2], 10) : null });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function isAllowed(rel: string, lineno: number, allow: readonly AllowEntry[]): boolean {
|
|
94
|
+
return allow.some((a) => a.file === rel && (a.line === null || a.line === lineno));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function isExcluded(rel: string): boolean {
|
|
98
|
+
return DEFAULT_EXCLUDE.some((rx) => rx.test(rel));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Fast binary sniff — a NUL byte in the first 8 KiB means "not text". */
|
|
102
|
+
function isBinary(abs: string): boolean {
|
|
103
|
+
try {
|
|
104
|
+
const fd = fs.openSync(abs, 'r');
|
|
105
|
+
const buf = Buffer.alloc(8192);
|
|
106
|
+
const n = fs.readSync(fd, buf, 0, 8192, 0);
|
|
107
|
+
fs.closeSync(fd);
|
|
108
|
+
return buf.subarray(0, n).includes(0);
|
|
109
|
+
} catch {
|
|
110
|
+
return true; // unreadable → skip
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function gitLines(root: string, args: readonly string[]): string[] {
|
|
115
|
+
const res = spawnSync('git', args as string[], { cwd: root, encoding: 'utf-8' });
|
|
116
|
+
if (res.status !== 0) {
|
|
117
|
+
return [];
|
|
118
|
+
}
|
|
119
|
+
return res.stdout.split('\n').filter((l) => l.trim() !== '');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export type Mode = 'diff' | 'all' | 'explicit';
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Resolve the file set.
|
|
126
|
+
* explicit → the named paths (intent — excludes skipped by the caller).
|
|
127
|
+
* all → every tracked file (`git ls-files`).
|
|
128
|
+
* diff → files this branch adds or changes vs `base` PLUS untracked files —
|
|
129
|
+
* the shift-left surface: scan what the change introduces, never the
|
|
130
|
+
* pre-existing corpus (which floods a mature repo with matches).
|
|
131
|
+
*/
|
|
132
|
+
export function resolveFiles(
|
|
133
|
+
root: string,
|
|
134
|
+
mode: Mode,
|
|
135
|
+
opts: { explicit?: readonly string[] | undefined; base?: string | undefined } = {},
|
|
136
|
+
): string[] {
|
|
137
|
+
if (mode === 'explicit') {
|
|
138
|
+
return (opts.explicit ?? []).map((p) =>
|
|
139
|
+
path.isAbsolute(p) ? path.relative(root, p) : p,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
if (mode === 'all') {
|
|
143
|
+
return gitLines(root, ['ls-files']);
|
|
144
|
+
}
|
|
145
|
+
const base = opts.base ?? 'origin/main';
|
|
146
|
+
// An unresolvable base is a scan of NOTHING, not a clean scan.
|
|
147
|
+
//
|
|
148
|
+
// `git diff <missing-ref>` yields no lines, so the changed-set silently
|
|
149
|
+
// became empty and the gate reported "no high-confidence secret found in the
|
|
150
|
+
// tracked tree" while examining zero files. That is the shape a shallow CI
|
|
151
|
+
// checkout produces by default — verified: with an unresolvable base this
|
|
152
|
+
// function returned 0 paths. A secret gate that passes green on an empty
|
|
153
|
+
// scan is worse than no gate, because it is believed.
|
|
154
|
+
const probe = spawnSync('git', ['-C', root, 'rev-parse', '--verify', '--quiet', `${base}^{commit}`], {
|
|
155
|
+
encoding: 'utf-8',
|
|
156
|
+
});
|
|
157
|
+
if ((probe.status ?? 1) !== 0) {
|
|
158
|
+
throw new Error(
|
|
159
|
+
`check_secret_leak: baseline ref '${base}' does not resolve, so the changed-set would be empty ` +
|
|
160
|
+
`and the scan would examine no files. Fetch the branch (CI: actions/checkout with fetch-depth: 0), ` +
|
|
161
|
+
`pass --base <ref>, or use --all to scan the whole tracked tree.`,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
const changed = gitLines(root, ['diff', '--name-only', '--diff-filter=ACMR', base]);
|
|
165
|
+
const untracked = gitLines(root, ['ls-files', '--others', '--exclude-standard']);
|
|
166
|
+
return Array.from(new Set([...changed, ...untracked]));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function scanRepo(
|
|
170
|
+
root: string,
|
|
171
|
+
mode: Mode = 'diff',
|
|
172
|
+
opts: { explicit?: readonly string[] | undefined; base?: string | undefined } = {},
|
|
173
|
+
): LeakHit[] {
|
|
174
|
+
const allow = readAllowFile(root);
|
|
175
|
+
// Explicit paths express intent — the fixture/test/example excludes (which
|
|
176
|
+
// exist so an auto-discovered scan ignores intentional fake secrets) are
|
|
177
|
+
// skipped only when the caller names paths directly.
|
|
178
|
+
const applyExcludes = mode !== 'explicit';
|
|
179
|
+
const hits: LeakHit[] = [];
|
|
180
|
+
for (const rel of resolveFiles(root, mode, opts)) {
|
|
181
|
+
if (applyExcludes && isExcluded(rel)) {
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
const abs = path.join(root, rel);
|
|
185
|
+
if (!fs.existsSync(abs) || !fs.statSync(abs).isFile() || isBinary(abs)) {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
let text: string;
|
|
189
|
+
try {
|
|
190
|
+
text = fs.readFileSync(abs, 'utf-8');
|
|
191
|
+
} catch {
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
for (const f of scanText(text, { path: rel })) {
|
|
195
|
+
if (f.confidence !== 'high') {
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (isAllowed(rel, f.line, allow)) {
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
hits.push({ ...f, file: rel });
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return hits;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
interface Args {
|
|
208
|
+
json: boolean;
|
|
209
|
+
all: boolean;
|
|
210
|
+
base: string | undefined;
|
|
211
|
+
paths: string[];
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function parseArgs(argv: readonly string[]): Args {
|
|
215
|
+
const paths: string[] = [];
|
|
216
|
+
let json = false;
|
|
217
|
+
let all = false;
|
|
218
|
+
let base: string | undefined;
|
|
219
|
+
for (let i = 0; i < argv.length; i++) {
|
|
220
|
+
const a = argv[i] as string;
|
|
221
|
+
if (a === '--json') {
|
|
222
|
+
json = true;
|
|
223
|
+
} else if (a === '--all') {
|
|
224
|
+
all = true;
|
|
225
|
+
} else if (a === '--base') {
|
|
226
|
+
base = argv[++i];
|
|
227
|
+
} else if (a === '-h' || a === '--help') {
|
|
228
|
+
_stdout(
|
|
229
|
+
'usage: check_secret_leak [paths...] [--all] [--base <ref>] [--json]\n' +
|
|
230
|
+
' default → scans files changed vs origin/main + untracked (shift-left).\n' +
|
|
231
|
+
' --all → scans the whole tracked tree.\n' +
|
|
232
|
+
' paths → scans exactly those paths (excludes skipped).\n' +
|
|
233
|
+
' Exit 1 on a high-confidence leak.\n',
|
|
234
|
+
);
|
|
235
|
+
process.exit(0);
|
|
236
|
+
} else if (a.startsWith('--')) {
|
|
237
|
+
_stderr(`check_secret_leak: unknown flag ${a}\n`);
|
|
238
|
+
process.exit(2);
|
|
239
|
+
} else {
|
|
240
|
+
paths.push(a);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return { json, all, base, paths };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function _stdout(s: string): void {
|
|
247
|
+
process.stdout.write(s);
|
|
248
|
+
}
|
|
249
|
+
function _stderr(s: string): void {
|
|
250
|
+
process.stderr.write(s);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function main(argv?: readonly string[]): number {
|
|
254
|
+
const args = parseArgs(argv ?? process.argv.slice(2));
|
|
255
|
+
const mode: Mode = args.paths.length > 0 ? 'explicit' : args.all ? 'all' : 'diff';
|
|
256
|
+
const hits = scanRepo(REPO_ROOT, mode, { explicit: args.paths, base: args.base });
|
|
257
|
+
|
|
258
|
+
if (args.json) {
|
|
259
|
+
_stdout(JSON.stringify({ check: 'secret-leak', count: hits.length, hits }, null, 2) + '\n');
|
|
260
|
+
} else if (hits.length === 0) {
|
|
261
|
+
_stdout('✅ check_secret_leak: no high-confidence secret found in the tracked tree.\n');
|
|
262
|
+
} else {
|
|
263
|
+
_stderr(`❌ check_secret_leak: ${hits.length} high-confidence secret finding(s):\n`);
|
|
264
|
+
for (const h of hits) {
|
|
265
|
+
_stderr(` ${h.file}:${h.line} ${h.kind} (${h.masked})\n`);
|
|
266
|
+
}
|
|
267
|
+
_stderr(
|
|
268
|
+
'\nRotate any real credential NOW (a commit does not un-leak it), move it to a\n' +
|
|
269
|
+
'secret store (see skill:secrets-management), or add an audited entry to\n' +
|
|
270
|
+
'.secret-allow / an inline `# secret-allow` marker if this is a false positive.\n',
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
return hits.length === 0 ? 0 : 1;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Main-guard (realpath-compared, mirrors the repo convention).
|
|
277
|
+
if (process.argv[1] !== undefined) {
|
|
278
|
+
try {
|
|
279
|
+
const here = fs.realpathSync(fileURLToPath(import.meta.url));
|
|
280
|
+
const argv1 = fs.realpathSync(path.resolve(process.argv[1]));
|
|
281
|
+
if (here === argv1) {
|
|
282
|
+
process.exit(main());
|
|
283
|
+
}
|
|
284
|
+
} catch {
|
|
285
|
+
const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
286
|
+
if (import.meta.url === argvUrl) {
|
|
287
|
+
process.exit(main());
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* check_source_pointer_freshness — fail when an AUTHORING artifact asserts a
|
|
4
|
+
* RETIRED source-of-truth pointer as the current one.
|
|
5
|
+
*
|
|
6
|
+
* The doc-follows-code discipline (rule: downstream-changes § Doc-Impact;
|
|
7
|
+
* skill: agent-docs-writing § Doc-Impact) is a behavioral obligation. This is
|
|
8
|
+
* its deterministic backstop for the one drift class a plain script CAN catch
|
|
9
|
+
* without semantics: a doc that still names the retired `.agent-src.uncondensed/`
|
|
10
|
+
* tree as the source of truth after the source moved to `src/` (ADR-051).
|
|
11
|
+
*
|
|
12
|
+
* Scope is deliberately narrow (council 2026-07-21, do-not-build list): an
|
|
13
|
+
* explicit allowlist of the authoring files that MAKE a source-of-truth claim,
|
|
14
|
+
* not a whole-tree sweep — a gate that fires on every commit and gets
|
|
15
|
+
* routinely overridden is worse than none. Historical mentions (ADRs under
|
|
16
|
+
* docs/decisions/, archived roadmaps) are out of scope by construction; a
|
|
17
|
+
* genuinely-historical line inside an allowlisted file is exempted with an
|
|
18
|
+
* inline `<!-- pointer-freshness: historical -->` marker.
|
|
19
|
+
*
|
|
20
|
+
* Extend AUTHORING_FILES as the broader `.agent-src.uncondensed` cleanup lands
|
|
21
|
+
* (186 files still carry the token; see road-to-doc-follows-code Phase 4/5
|
|
22
|
+
* follow-up).
|
|
23
|
+
*
|
|
24
|
+
* Exit codes: 0 = clean, 1 = a retired pointer in an allowlisted authoring
|
|
25
|
+
* file, 2 = usage / self-test failure.
|
|
26
|
+
*
|
|
27
|
+
* Usage:
|
|
28
|
+
* tsx src/scripts/check_source_pointer_freshness.ts [--json] [--selftest]
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import * as fs from 'node:fs';
|
|
32
|
+
import * as path from 'node:path';
|
|
33
|
+
import process from 'node:process';
|
|
34
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
35
|
+
|
|
36
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
37
|
+
// parents[2] of src/scripts/<file> is the repo root.
|
|
38
|
+
const ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
39
|
+
|
|
40
|
+
// Authoring files that assert WHERE the source of truth lives. Each must NOT
|
|
41
|
+
// name the retired `.agent-src.uncondensed/` tree as current. Extend as the
|
|
42
|
+
// broader cleanup lands.
|
|
43
|
+
const AUTHORING_FILES: readonly string[] = [
|
|
44
|
+
'src/agent-src/README.md',
|
|
45
|
+
'src/skills/agents-md-thin-root/SKILL.md',
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
// Retired source-of-truth pointer. Matches both the bare token and the older
|
|
49
|
+
// monorepo-prefixed form `packages/core/.agent-src.uncondensed/`.
|
|
50
|
+
const RETIRED_POINTER = /\.agent-src\.uncondensed\//;
|
|
51
|
+
|
|
52
|
+
// A line carrying this marker is a deliberate historical reference — exempt.
|
|
53
|
+
const HISTORICAL_MARKER = '<!-- pointer-freshness: historical -->';
|
|
54
|
+
|
|
55
|
+
interface Hit {
|
|
56
|
+
file: string;
|
|
57
|
+
line: number;
|
|
58
|
+
text: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function _scanFile(rel: string): Hit[] {
|
|
62
|
+
const abs = path.join(ROOT, rel);
|
|
63
|
+
let text: string;
|
|
64
|
+
try {
|
|
65
|
+
if (!fs.statSync(abs).isFile()) {
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
text = fs.readFileSync(abs, 'utf-8');
|
|
69
|
+
} catch {
|
|
70
|
+
// A missing allowlisted file is a config drift in itself — surface it.
|
|
71
|
+
return [{ file: rel, line: 0, text: '(allowlisted authoring file not found)' }];
|
|
72
|
+
}
|
|
73
|
+
const hits: Hit[] = [];
|
|
74
|
+
const lines = text.split('\n');
|
|
75
|
+
for (let idx = 0; idx < lines.length; idx += 1) {
|
|
76
|
+
const line = lines[idx] as string;
|
|
77
|
+
if (line.includes(HISTORICAL_MARKER)) {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (RETIRED_POINTER.test(line)) {
|
|
81
|
+
hits.push({ file: rel, line: idx + 1, text: line.trim().slice(0, 160) });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return hits;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Deterministic self-check of the matcher — no filesystem, no network. */
|
|
88
|
+
function _selftest(): number {
|
|
89
|
+
const stale = 'The source of truth is `.agent-src.uncondensed/`. Edit there.';
|
|
90
|
+
const monorepo = 'Editing `packages/core/.agent-src.uncondensed/templates/AGENTS.md`.';
|
|
91
|
+
const clean = 'The source of truth is `src/`. Edit there, then run `task sync`.';
|
|
92
|
+
const historical = `Retired path \`.agent-src.uncondensed/\` ${HISTORICAL_MARKER}`;
|
|
93
|
+
const cases: Array<[string, boolean]> = [
|
|
94
|
+
[stale, true],
|
|
95
|
+
[monorepo, true],
|
|
96
|
+
[clean, false],
|
|
97
|
+
[historical, false], // marker suppresses the match at scan time
|
|
98
|
+
];
|
|
99
|
+
let ok = true;
|
|
100
|
+
for (const [sample, shouldMatch] of cases) {
|
|
101
|
+
const exempt = sample.includes(HISTORICAL_MARKER);
|
|
102
|
+
const matched = !exempt && RETIRED_POINTER.test(sample);
|
|
103
|
+
if (matched !== shouldMatch) {
|
|
104
|
+
process.stderr.write(`selftest FAIL: ${JSON.stringify(sample)} expected match=${shouldMatch}, got ${matched}\n`);
|
|
105
|
+
ok = false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (ok) {
|
|
109
|
+
process.stdout.write('✅ check_source_pointer_freshness selftest passed (4/4).\n');
|
|
110
|
+
return 0;
|
|
111
|
+
}
|
|
112
|
+
return 2;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function main(argv: readonly string[]): number {
|
|
116
|
+
if (argv.includes('--selftest')) {
|
|
117
|
+
return _selftest();
|
|
118
|
+
}
|
|
119
|
+
const asJson = argv.includes('--json');
|
|
120
|
+
const hits: Hit[] = [];
|
|
121
|
+
for (const rel of AUTHORING_FILES) {
|
|
122
|
+
hits.push(..._scanFile(rel));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (asJson) {
|
|
126
|
+
process.stdout.write(JSON.stringify({ ok: hits.length === 0, hits }, null, 2) + '\n');
|
|
127
|
+
} else if (hits.length > 0) {
|
|
128
|
+
process.stdout.write(`❌ ${hits.length} retired source-of-truth pointer(s) in authoring files:\n\n`);
|
|
129
|
+
for (const h of hits) {
|
|
130
|
+
process.stdout.write(` ${h.file}:${h.line} ${h.text}\n`);
|
|
131
|
+
}
|
|
132
|
+
process.stdout.write(
|
|
133
|
+
'\nThese name the retired `.agent-src.uncondensed/` tree as the source of\n' +
|
|
134
|
+
'truth. The source of truth is `src/` (ADR-051, rule: source-of-truth).\n' +
|
|
135
|
+
'Fix the pointer, or mark a genuinely-historical line with\n' +
|
|
136
|
+
`\`${HISTORICAL_MARKER}\`. See rule: downstream-changes § Doc-Impact.\n`,
|
|
137
|
+
);
|
|
138
|
+
} else {
|
|
139
|
+
process.stdout.write('✅ No retired source-of-truth pointers in authoring files.\n');
|
|
140
|
+
}
|
|
141
|
+
return hits.length > 0 ? 1 : 0;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function _isCliEntry(): boolean {
|
|
145
|
+
if (process.argv[1] === undefined) {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
149
|
+
if (import.meta.url === argvUrl) {
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
try {
|
|
153
|
+
const here = fs.realpathSync(fileURLToPath(import.meta.url));
|
|
154
|
+
const argv = fs.realpathSync(path.resolve(process.argv[1]));
|
|
155
|
+
return here === argv;
|
|
156
|
+
} catch {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (_isCliEntry()) {
|
|
162
|
+
process.exit(main(process.argv.slice(2)));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export { main, _scanFile, _selftest, AUTHORING_FILES, RETIRED_POINTER, HISTORICAL_MARKER };
|
|
Binary file
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Native code-graph engine CLI (Class-A, ADR-124).
|
|
4
|
+
*
|
|
5
|
+
* build [--root P] [--out P] [--force] (re)build the deterministic graph
|
|
6
|
+
* validate [--graph P] schema-gate a graph.json
|
|
7
|
+
*
|
|
8
|
+
* Query subcommands (query/path/explain/affected/detect) are added by the
|
|
9
|
+
* Phase-3 query tier. Deterministic, LLM-free, no network. Exit codes:
|
|
10
|
+
* 0 ok · 1 not-found / validation-failed · 2 usage · 3 internal.
|
|
11
|
+
*/
|
|
12
|
+
import { execFileSync } from 'node:child_process';
|
|
13
|
+
import * as fs from 'node:fs';
|
|
14
|
+
import * as path from 'node:path';
|
|
15
|
+
import process from 'node:process';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
|
|
18
|
+
import { hardenedSpawnEnv } from '../_lib/spawn_env.js';
|
|
19
|
+
import { buildFromRepo } from './build.js';
|
|
20
|
+
import { detectSources, pickSource } from './detect.js';
|
|
21
|
+
import { affected, explain, loadGraph, type LoadedGraph, path as graphPath, query, type QueryResult } from './query.js';
|
|
22
|
+
import { validateGraph } from './validate.js';
|
|
23
|
+
|
|
24
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
25
|
+
export const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..', '..');
|
|
26
|
+
export const DEFAULT_CACHE = path.join(REPO_ROOT, 'agents', 'runtime', 'state', 'code-graph-v1.json');
|
|
27
|
+
|
|
28
|
+
function flag(argv: string[], name: string): string | null {
|
|
29
|
+
const i = argv.indexOf(name);
|
|
30
|
+
return i >= 0 && i + 1 < argv.length ? (argv[i + 1] as string) : null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function cmdBuild(argv: string[]): Promise<number> {
|
|
34
|
+
const root = flag(argv, '--root') ?? REPO_ROOT;
|
|
35
|
+
const out = argv.includes('--out') ? flag(argv, '--out') : DEFAULT_CACHE;
|
|
36
|
+
if (!fs.existsSync(root)) {
|
|
37
|
+
process.stderr.write(`code-graph: root not found: ${root}\n`);
|
|
38
|
+
return 1;
|
|
39
|
+
}
|
|
40
|
+
const update = argv.includes('--update');
|
|
41
|
+
const { graph, fileCount, reExtracted, reused } = await buildFromRepo(root, out, { update });
|
|
42
|
+
const c = graph.edge_confidence_counts;
|
|
43
|
+
process.stdout.write(
|
|
44
|
+
`✅ code-graph ${update ? 'updated' : 'built'} — ${fileCount} files · ${graph.nodes.length} nodes · ${graph.edges.length} edges\n` +
|
|
45
|
+
` languages: ${graph.languages.join(', ') || '(none)'} · grammar ABI ${graph.grammar_abi}\n` +
|
|
46
|
+
` edges: EXTRACTED ${c.EXTRACTED} · INFERRED ${c.INFERRED} · AMBIGUOUS ${c.AMBIGUOUS}\n` +
|
|
47
|
+
(update ? ` incremental: ${reExtracted} re-extracted · ${reused} reused\n` : '') +
|
|
48
|
+
(out ? ` cache: ${path.relative(REPO_ROOT, out)}\n` : ''),
|
|
49
|
+
);
|
|
50
|
+
return 0;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function cmdValidate(argv: string[]): number {
|
|
54
|
+
const p = flag(argv, '--graph') ?? DEFAULT_CACHE;
|
|
55
|
+
if (!fs.existsSync(p)) {
|
|
56
|
+
process.stderr.write(`code-graph: graph not found: ${p}\n`);
|
|
57
|
+
return 1;
|
|
58
|
+
}
|
|
59
|
+
let parsed: unknown;
|
|
60
|
+
try {
|
|
61
|
+
parsed = JSON.parse(fs.readFileSync(p, 'utf-8'));
|
|
62
|
+
} catch (e) {
|
|
63
|
+
process.stderr.write(`code-graph: graph is not valid JSON: ${(e as Error).message}\n`);
|
|
64
|
+
return 1;
|
|
65
|
+
}
|
|
66
|
+
const r = validateGraph(parsed);
|
|
67
|
+
if (r.ok) {
|
|
68
|
+
process.stdout.write(`✅ graph schema valid: ${path.relative(REPO_ROOT, p)}\n`);
|
|
69
|
+
return 0;
|
|
70
|
+
}
|
|
71
|
+
process.stderr.write(`❌ graph schema invalid (${r.errors.length}):\n`);
|
|
72
|
+
for (const e of r.errors) process.stderr.write(` ${e}\n`);
|
|
73
|
+
return 1;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function resolveGraph(argv: string[]): { g: LoadedGraph; note: string } | { err: string } {
|
|
77
|
+
const explicit = flag(argv, '--graph');
|
|
78
|
+
const root = flag(argv, '--root') ?? REPO_ROOT;
|
|
79
|
+
if (explicit) {
|
|
80
|
+
if (!fs.existsSync(explicit)) return { err: `graph not found: ${explicit}` };
|
|
81
|
+
return { g: loadGraph(explicit, path.relative(REPO_ROOT, explicit)), note: '' };
|
|
82
|
+
}
|
|
83
|
+
const picked = pickSource(detectSources(root, DEFAULT_CACHE));
|
|
84
|
+
if (!picked) return { err: 'no code-graph source found — run `code_graph build` or ship a graph.json' };
|
|
85
|
+
if (picked.kind === 'scip') return { err: `${picked.note ?? 'SCIP detected'} — pass --graph <json> or install peer tooling` };
|
|
86
|
+
const staleNote = picked.stale ? ` ⚠ index is ${picked.commits_behind ?? 'N'} commit(s) behind — rebuild before trusting` : '';
|
|
87
|
+
return { g: loadGraph(picked.path, `${picked.kind}:${path.relative(REPO_ROOT, picked.path)}`), note: staleNote };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function render(r: QueryResult, staleNote: string): number {
|
|
91
|
+
process.stdout.write(`source: ${r.source}${staleNote}\n`);
|
|
92
|
+
if (r.seeds.length) process.stdout.write(`seeds: ${r.seeds.join(', ')}\n`);
|
|
93
|
+
for (const l of r.lines) process.stdout.write(` ${l}\n`);
|
|
94
|
+
if (r.truncated) process.stdout.write(' … (budget reached — narrow the seed or raise --budget)\n');
|
|
95
|
+
if (!r.lines.length) process.stdout.write(' (no matching relations)\n');
|
|
96
|
+
return 0;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function budgetOf(argv: string[]): number {
|
|
100
|
+
const b = Number(flag(argv, '--budget'));
|
|
101
|
+
return Number.isFinite(b) && b > 0 ? b : 1500;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function changedNodeSeeds(g: LoadedGraph, root: string, ref: string): string[] {
|
|
105
|
+
let files: string[];
|
|
106
|
+
try {
|
|
107
|
+
files = execFileSync('git', ['-C', root, 'diff', '--name-only', `${ref}..HEAD`], {
|
|
108
|
+
env: hardenedSpawnEnv(),
|
|
109
|
+
encoding: 'utf-8',
|
|
110
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
111
|
+
})
|
|
112
|
+
.split('\n')
|
|
113
|
+
.map((s) => s.trim())
|
|
114
|
+
.filter(Boolean);
|
|
115
|
+
} catch {
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
const set = new Set(files);
|
|
119
|
+
return g.graph.nodes.filter((n) => set.has(n.source_file) && n.kind !== 'file').map((n) => n.id);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function cmdQuery(kind: 'query' | 'explain' | 'affected' | 'path', argv: string[]): number {
|
|
123
|
+
const r = resolveGraph(argv);
|
|
124
|
+
if ('err' in r) {
|
|
125
|
+
process.stderr.write(`code-graph: ${r.err}\n`);
|
|
126
|
+
return 1;
|
|
127
|
+
}
|
|
128
|
+
const positional = argv.filter((a) => !a.startsWith('--') && !isFlagValue(argv, a));
|
|
129
|
+
const budget = budgetOf(argv);
|
|
130
|
+
if (kind === 'affected' && flag(argv, '--since')) {
|
|
131
|
+
const root = flag(argv, '--root') ?? REPO_ROOT;
|
|
132
|
+
const seeds = changedNodeSeeds(r.g, root, flag(argv, '--since') as string);
|
|
133
|
+
const merged: QueryResult = { source: r.g.source, seeds: [], lines: [], truncated: false };
|
|
134
|
+
for (const s of seeds) {
|
|
135
|
+
const one = affected(r.g, s, 2, budget);
|
|
136
|
+
merged.seeds.push(...one.seeds);
|
|
137
|
+
merged.lines.push(...one.lines);
|
|
138
|
+
}
|
|
139
|
+
return render(merged, r.note);
|
|
140
|
+
}
|
|
141
|
+
const a = positional[0];
|
|
142
|
+
if (!a) {
|
|
143
|
+
process.stderr.write(`usage: code_graph/cli.ts ${kind} <symbol> [--graph P] [--budget N]\n`);
|
|
144
|
+
return 2;
|
|
145
|
+
}
|
|
146
|
+
if (kind === 'path') {
|
|
147
|
+
const b = positional[1];
|
|
148
|
+
if (!b) {
|
|
149
|
+
process.stderr.write('usage: code_graph/cli.ts path <a> <b>\n');
|
|
150
|
+
return 2;
|
|
151
|
+
}
|
|
152
|
+
return render(graphPath(r.g, a, b, budget), r.note);
|
|
153
|
+
}
|
|
154
|
+
const fn = kind === 'query' ? query : kind === 'explain' ? explain : affected;
|
|
155
|
+
return render(fn(r.g, a, budget), r.note);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function isFlagValue(argv: string[], token: string): boolean {
|
|
159
|
+
const i = argv.indexOf(token);
|
|
160
|
+
return i > 0 && (argv[i - 1] as string).startsWith('--');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function cmdDetect(argv: string[]): number {
|
|
164
|
+
const root = flag(argv, '--root') ?? REPO_ROOT;
|
|
165
|
+
const verdicts = detectSources(root, DEFAULT_CACHE);
|
|
166
|
+
if (!verdicts.length) {
|
|
167
|
+
process.stdout.write('no code-graph source detected\n');
|
|
168
|
+
return 1;
|
|
169
|
+
}
|
|
170
|
+
for (const v of verdicts) {
|
|
171
|
+
const fresh = v.stale === undefined ? '' : v.stale ? ` · STALE${v.commits_behind !== undefined ? ` (${v.commits_behind} behind)` : ''}` : ' · fresh';
|
|
172
|
+
process.stdout.write(`${v.kind.padEnd(9)} ${path.relative(REPO_ROOT, v.path)}${fresh}${v.note ? ` · ${v.note}` : ''}\n`);
|
|
173
|
+
}
|
|
174
|
+
const picked = pickSource(verdicts);
|
|
175
|
+
if (picked) process.stdout.write(`→ query would use: ${picked.kind}\n`);
|
|
176
|
+
return 0;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export async function main(argv: string[] = process.argv.slice(2)): Promise<number> {
|
|
180
|
+
const sub = argv[0];
|
|
181
|
+
try {
|
|
182
|
+
if (sub === 'build') return await cmdBuild(argv.slice(1));
|
|
183
|
+
if (sub === 'validate') return cmdValidate(argv.slice(1));
|
|
184
|
+
if (sub === 'detect') return cmdDetect(argv.slice(1));
|
|
185
|
+
if (sub === 'query' || sub === 'explain' || sub === 'affected' || sub === 'path')
|
|
186
|
+
return cmdQuery(sub, argv.slice(1));
|
|
187
|
+
process.stderr.write(
|
|
188
|
+
'usage: code_graph/cli.ts <build|validate|detect|query|explain|affected|path> [options]\n',
|
|
189
|
+
);
|
|
190
|
+
return 2;
|
|
191
|
+
} catch (e) {
|
|
192
|
+
process.stderr.write(`code-graph: internal error: ${(e as Error).message}\n`);
|
|
193
|
+
return 3;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const _invokedDirectly = (() => {
|
|
198
|
+
if (process.argv[1] === undefined) return false;
|
|
199
|
+
try {
|
|
200
|
+
return fs.realpathSync(path.resolve(process.argv[1])) === fs.realpathSync(_HERE);
|
|
201
|
+
} catch {
|
|
202
|
+
return import.meta.url === `file://${process.argv[1]}`;
|
|
203
|
+
}
|
|
204
|
+
})();
|
|
205
|
+
if (_invokedDirectly) {
|
|
206
|
+
main().then((rc) => process.exit(rc));
|
|
207
|
+
}
|