@event4u/agent-config 9.7.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 +144 -0
- package/README.md +5 -5
- 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/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/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/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 +161 -44
- 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/registry-manifest.json +3 -3
- 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 +62 -37
- package/docs/architecture.md +2 -2
- package/docs/benchmark.md +137 -0
- 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-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/threat-model.md +1 -0
- package/package.json +5 -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/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/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/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_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/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/iron_law_sha.ts +3 -12
- package/src/scripts/lint_override_kernel_guard.ts +219 -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
|
+
}
|
|
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
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Source detection + freshness. Answers "what code-graph source should a
|
|
3
|
+
* query use, and is it fresh?" across three sources:
|
|
4
|
+
*
|
|
5
|
+
* consumer — a `graph.json`-shaped artifact the consumer repo ships
|
|
6
|
+
* scip — an `index.scip` / `*.scip` (presence only; peer tooling
|
|
7
|
+
* required — an owned SCIP reader is YAGNI-gated until a
|
|
8
|
+
* consumer actually ships one)
|
|
9
|
+
* native — the suite's own gitignored cache
|
|
10
|
+
*
|
|
11
|
+
* Precedence (ADR-124 § 2): a fresh consumer index wins (interop courtesy);
|
|
12
|
+
* the native engine covers stale-or-absent. Freshness: `head_at_build` when
|
|
13
|
+
* the artifact embeds a SHA (→ commits_behind), else artifact mtime vs the
|
|
14
|
+
* repo's last commit time. No guessing.
|
|
15
|
+
*/
|
|
16
|
+
import { execFileSync } from 'node:child_process';
|
|
17
|
+
import * as fs from 'node:fs';
|
|
18
|
+
import * as path from 'node:path';
|
|
19
|
+
|
|
20
|
+
import { hardenedSpawnEnv } from '../_lib/spawn_env.js';
|
|
21
|
+
import { validateGraph } from './validate.js';
|
|
22
|
+
|
|
23
|
+
export type SourceKind = 'consumer' | 'scip' | 'native';
|
|
24
|
+
|
|
25
|
+
export interface SourceVerdict {
|
|
26
|
+
kind: SourceKind;
|
|
27
|
+
path: string;
|
|
28
|
+
present: boolean;
|
|
29
|
+
stale?: boolean;
|
|
30
|
+
commits_behind?: number;
|
|
31
|
+
note?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const CONSUMER_CANDIDATES = ['graph.json', 'code-graph.json', '.code-graph/graph.json'];
|
|
35
|
+
|
|
36
|
+
function gitLastCommitEpoch(root: string): number | null {
|
|
37
|
+
try {
|
|
38
|
+
const out = execFileSync('git', ['-C', root, 'log', '-1', '--format=%ct'], {
|
|
39
|
+
env: hardenedSpawnEnv(),
|
|
40
|
+
encoding: 'utf-8',
|
|
41
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
42
|
+
}).trim();
|
|
43
|
+
const n = Number(out);
|
|
44
|
+
return Number.isFinite(n) ? n : null;
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function commitsBehind(root: string, sha: string): number | null {
|
|
51
|
+
try {
|
|
52
|
+
const out = execFileSync('git', ['-C', root, 'rev-list', '--count', `${sha}..HEAD`], {
|
|
53
|
+
env: hardenedSpawnEnv(),
|
|
54
|
+
encoding: 'utf-8',
|
|
55
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
56
|
+
}).trim();
|
|
57
|
+
const n = Number(out);
|
|
58
|
+
return Number.isFinite(n) ? n : null;
|
|
59
|
+
} catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function freshness(root: string, file: string, embeddedSha: string | null): { stale?: boolean; commits_behind?: number } {
|
|
65
|
+
if (embeddedSha) {
|
|
66
|
+
const behind = commitsBehind(root, embeddedSha);
|
|
67
|
+
if (behind !== null) return { stale: behind > 0, commits_behind: behind };
|
|
68
|
+
}
|
|
69
|
+
const last = gitLastCommitEpoch(root);
|
|
70
|
+
if (last === null) return {};
|
|
71
|
+
try {
|
|
72
|
+
const mtime = Math.floor(fs.statSync(file).mtimeMs / 1000);
|
|
73
|
+
return { stale: mtime < last };
|
|
74
|
+
} catch {
|
|
75
|
+
return {};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Shape-validate a candidate consumer graph.json (native or foreign shape). */
|
|
80
|
+
function looksLikeGraph(p: string): { ok: boolean; sha: string | null } {
|
|
81
|
+
try {
|
|
82
|
+
const raw = JSON.parse(fs.readFileSync(p, 'utf-8')) as Record<string, unknown>;
|
|
83
|
+
// native shape passes validateGraph; a foreign graph.json just needs
|
|
84
|
+
// nodes[] + edges|links[] with source/target/relation.
|
|
85
|
+
if (validateGraph(raw).ok) return { ok: true, sha: (raw['head_at_build'] as string) ?? null };
|
|
86
|
+
const nodes = raw['nodes'];
|
|
87
|
+
const edges = (raw['edges'] ?? raw['links']) as unknown;
|
|
88
|
+
const okShape =
|
|
89
|
+
Array.isArray(nodes) &&
|
|
90
|
+
Array.isArray(edges) &&
|
|
91
|
+
(edges.length === 0 ||
|
|
92
|
+
(typeof edges[0] === 'object' && edges[0] !== null && 'source' in (edges[0] as object) && 'target' in (edges[0] as object)));
|
|
93
|
+
return { ok: okShape, sha: (raw['head_at_build'] as string) ?? null };
|
|
94
|
+
} catch {
|
|
95
|
+
return { ok: false, sha: null };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function detectSources(root: string, nativeCache: string): SourceVerdict[] {
|
|
100
|
+
const abs = path.resolve(root);
|
|
101
|
+
const out: SourceVerdict[] = [];
|
|
102
|
+
|
|
103
|
+
for (const rel of CONSUMER_CANDIDATES) {
|
|
104
|
+
const p = path.join(abs, rel);
|
|
105
|
+
if (fs.existsSync(p)) {
|
|
106
|
+
const { ok, sha } = looksLikeGraph(p);
|
|
107
|
+
if (ok) out.push({ kind: 'consumer', path: p, present: true, ...freshness(abs, p, sha) });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// SCIP — presence only
|
|
112
|
+
const scipCandidates = [path.join(abs, 'index.scip')];
|
|
113
|
+
try {
|
|
114
|
+
for (const e of fs.readdirSync(abs)) if (e.endsWith('.scip')) scipCandidates.push(path.join(abs, e));
|
|
115
|
+
} catch {
|
|
116
|
+
/* ignore */
|
|
117
|
+
}
|
|
118
|
+
const scip = scipCandidates.find((p) => fs.existsSync(p));
|
|
119
|
+
if (scip) out.push({ kind: 'scip', path: scip, present: true, note: 'SCIP detected — peer tooling required (no owned reader)' });
|
|
120
|
+
|
|
121
|
+
// native cache
|
|
122
|
+
if (fs.existsSync(nativeCache)) out.push({ kind: 'native', path: nativeCache, present: true, ...freshness(abs, nativeCache, null) });
|
|
123
|
+
|
|
124
|
+
return out;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Pick the source a query should use: fresh consumer > native > stale consumer. */
|
|
128
|
+
export function pickSource(verdicts: SourceVerdict[]): SourceVerdict | null {
|
|
129
|
+
const consumer = verdicts.find((v) => v.kind === 'consumer' && v.stale !== true);
|
|
130
|
+
if (consumer) return consumer;
|
|
131
|
+
const native = verdicts.find((v) => v.kind === 'native');
|
|
132
|
+
if (native) return native;
|
|
133
|
+
return verdicts.find((v) => v.kind === 'consumer') ?? null;
|
|
134
|
+
}
|