fix(engine): a schemaless host is still a host, and OK is a real outcome
Two findings from org-ops census 03, both measured against the parser rather than read out of the regex. (a) URL_REF required :// or @host:, so `git.fromaitochitta.com/open/<name>` — the form a subtree instruction routinely uses — extracted nothing at all (llm-security/V3-UPGRADE.md:343). What makes a name resolvable is its position after a host, not the scheme in front of it. Two guards keep the widening from becoming the noise the scheme was masking: the host must end in a TLD-shaped label, and a candidate preceded by / is a path segment that merely contains a dot, not a host — so docs/v1.2/open/ and test/nav.golden/open/ stay silent and the API-endpoint rule from 0.3.0 is untouched. Measured before shipping across 1501 tracked Markdown files in 20 local clones: 14 lines changed verdict. 13 were references that had been invisible. The 14th was a defect this widening introduced — a markdown link whose display text repeats its own URL matched on both halves, printing one dead reference twice and inflating the count the new OK line offers as evidence. References are now deduplicated per name-and-line, so two names on one line, or one name on two lines, still count as two. (b) checkLinks emitted nothing on success, so "no dead references" and "the check never ran" were identical in the output — a sweep could not tell 19 clean repos from 19 unread ones. The file already applies "three outcomes, never two" to classifyRef; it now applies it to its own result. Zero enumerated files is LINKS-OPEN-REFS-UNAVAILABLE (SKIP), which is the honest name for what used to look like a pass. Caught by the gate against itself: the first draft of this release's CHANGELOG entry used a literal open/<name> placeholder and became a real LINK-DEAD ERROR. Rewritten, not exempted. 116 -> 129 tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NxP9N3p1fG6UYSB8rATBL6
This commit is contained in:
parent
c5c11f4b13
commit
c8cbeb97a5
2 changed files with 155 additions and 2 deletions
|
|
@ -79,7 +79,18 @@ export function normalizeRepoRef(raw) {
|
|||
// restriction, an API endpoint like `/api/v1/orgs/open/repos` also matches —
|
||||
// `open` there is the org argument to the API, and `repos` is the literal
|
||||
// resource segment, not a repo name (measured: catalog RUNBOOK.md:39, :114).
|
||||
const URL_REF = /(?::\/\/[^\s)\]"'`/]+\/|@[^\s:]+:)open\/([A-Za-z0-9._-]+)/g;
|
||||
//
|
||||
// The third alternative is the SCHEMELESS host: `git.fromaitochitta.com/open/x`
|
||||
// written without `https://`, which a subtree instruction routinely is
|
||||
// (measured false negative: llm-security/V3-UPGRADE.md:343 — the scheme was
|
||||
// doing work it was never entitled to, and its absence hid a WARN). What makes
|
||||
// a name resolvable is its position after a HOST, not the scheme in front of
|
||||
// it. Two guards keep that from becoming the noise the scheme was masking: the
|
||||
// host must end in a TLD-shaped label, and the lookbehind refuses a host
|
||||
// preceded by `/`, `.` or a word character — because that is a PATH segment
|
||||
// that merely contains a dot (`docs/v1.2/open/`, `test/nav.golden/open/`), not
|
||||
// a host. The API-endpoint rule survives untouched: `orgs` carries no dot.
|
||||
const URL_REF = /(?::\/\/[^\s)\]"'`/]+\/|@[^\s:]+:|(?<![A-Za-z0-9._/-])[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)*\.[A-Za-z]{2,}\/)open\/([A-Za-z0-9._-]+)/g;
|
||||
|
||||
export function extractOpenRefs(text) {
|
||||
const out = [];
|
||||
|
|
@ -101,10 +112,34 @@ export function classifyRef(name, register) {
|
|||
return 'unknown';
|
||||
}
|
||||
|
||||
// The same "three outcomes, never two" rule this file applies to classifyRef,
|
||||
// applied to the check's own result. Emitting nothing on success made "no dead
|
||||
// references" and "the check never ran" identical in the output, so a sweep
|
||||
// across the org could not tell 19 clean repos from 19 unread ones (measured:
|
||||
// org-ops census 03b). Silence is not a pass here either.
|
||||
export function checkLinks({ files }, register) {
|
||||
const entries = Object.entries(files ?? {});
|
||||
if (entries.length === 0) {
|
||||
return [{
|
||||
level: 'SKIP',
|
||||
code: 'LINKS-OPEN-REFS-UNAVAILABLE',
|
||||
msg: 'no files were enumerated — the `open/` reference check did not run',
|
||||
}];
|
||||
}
|
||||
const findings = [];
|
||||
for (const [path, text] of Object.entries(files ?? {})) {
|
||||
// One name, one line, one reference. `[host/open/x](https://host/open/x)`
|
||||
// puts the same reference on both halves of a markdown link and matched
|
||||
// twice once the schemaless host became legible (measured:
|
||||
// llm-security/V3-ANNOUNCEMENT.md:124). The key keeps name AND line, so two
|
||||
// different names on one line — or the same name on two lines — stay two.
|
||||
const seen = new Set();
|
||||
let checked = 0;
|
||||
for (const [path, text] of entries) {
|
||||
for (const ref of extractOpenRefs(text)) {
|
||||
const key = `${path}\n${ref.line}\n${ref.name}`;
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
checked += 1;
|
||||
const kind = classifyRef(ref.name, register);
|
||||
if (kind === 'repo') continue;
|
||||
if (kind === 'non-repo') {
|
||||
|
|
@ -124,6 +159,17 @@ export function checkLinks({ files }, register) {
|
|||
}
|
||||
}
|
||||
}
|
||||
// The count IS the evidence. An OK that cannot say how many references it
|
||||
// resolved is the same silence wearing a different level.
|
||||
if (findings.length === 0) {
|
||||
findings.push({
|
||||
level: 'OK',
|
||||
code: 'LINKS-OPEN-REFS',
|
||||
msg: checked === 0
|
||||
? `no \`open/\` references found in ${entries.length} scanned file(s)`
|
||||
: `${checked} \`open/\` reference(s) checked — every one resolves to a registered repo`,
|
||||
});
|
||||
}
|
||||
return findings;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue