Δ vs v0: ${diff.length?diff.join(", "):"no change"} Spans detected: ${spans.join(", ")||"none"} Qualification gate: ${gk} Evidence: auto ⟳ heuristic file-tree scan — structural signals (checks real files, not just names), not comprehension. Promote to code ✓ with a reader-agent audit.`;
+ // #64: report scaffolding exclusion and scan-cap truncation separately.
+ const scanScope=corpus.truncated?`${corpus.kept} of ${corpus.kept+corpus.truncated}`:`${corpus.kept}`;
+ const scanParts=[];
+ if(corpus.dropped) scanParts.push(`${corpus.dropped} scaffolding path${corpus.dropped===1?'':'s'} excluded`);
+ if(corpus.truncated) scanParts.push(`${corpus.truncated} beyond the ${OIA.SCAN_CAP}-file scan cap, not read`);
+ if(tj.truncated) scanParts.push('tree truncated by GitHub');
+ const scanNote=scanParts.length?` (${scanParts.join('; ')})`:'';
+ s.innerHTML=`Auto-scan of ${scanScope} project files${scanNote} · pushed ${(meta.pushed_at||'').slice(0,10)}
${hdr} ${mini}
Δ vs v0: ${diff.length?diff.join(", "):"no change"} Spans detected: ${spans.join(", ")||"none"} Qualification gate: ${gk} Evidence: auto ⟳ heuristic file-tree scan — structural signals (checks real files, not just names), not comprehension. Promote to code ✓ with a reader-agent audit.`;
const bdg=document.querySelector('#r'+apps.indexOf(a)+' .badge'); if(bdg){bdg.className='badge auto';bdg.textContent='auto ⟳';}
}catch(e){s.textContent="Scan failed (offline, CORS, or rate limit): "+e;}
}
diff --git a/docs/oia-signals.mjs b/docs/oia-signals.mjs
index 386632e..80ed41b 100644
--- a/docs/oia-signals.mjs
+++ b/docs/oia-signals.mjs
@@ -79,16 +79,31 @@ export function denoisePaths(paths){
* on the matrix row for display only. Adding it would be a scope change that
* promotes rows rather than de-noising them; propose it separately if wanted.
*/
+/** Upper bound on file paths read into the tree corpus. */
+export const SCAN_CAP = 6000;
+
export function buildCorpus({paths=[], readme='', topics=[], language=''} = {}){
- const lower = paths.map(p => String(p).toLowerCase());
- const kept = denoisePaths(lower).slice(0, 6000);
- const tree = kept.join(' ');
+ const lower = paths.map(p => String(p).toLowerCase());
+ const denoised = denoisePaths(lower);
+ const kept = denoised.slice(0, SCAN_CAP);
+ const tree = kept.join(' ');
const doc = [
String(readme||'').toLowerCase().slice(0, 40000),
(topics||[]).join(' ').toLowerCase(),
String(language||'').toLowerCase(),
].join(' ');
- return {tree, doc, all: tree + ' ' + doc, kept: kept.length, dropped: lower.length - kept.length};
+ // `dropped` and `truncated` are deliberately separate (#64). They used to be
+ // summed into one counter computed against the pre-cap length, which the
+ // narrative then reported wholly as scaffolding. On a repo larger than
+ // SCAN_CAP that published a scaffolding count off by two orders of magnitude
+ // and hid the fact that most of the tree was never read at all.
+ // kept + dropped + truncated partitions the input.
+ return {
+ tree, doc, all: tree + ' ' + doc,
+ kept: kept.length,
+ dropped: lower.length - denoised.length,
+ truncated: denoised.length - kept.length,
+ };
}
/** How many regexes in a group match the corpus (presence per regex, not total hits). */
diff --git a/scripts/classify-oia.mjs b/scripts/classify-oia.mjs
index 18be6b9..f9855ec 100644
--- a/scripts/classify-oia.mjs
+++ b/scripts/classify-oia.mjs
@@ -6,7 +6,7 @@
// comprehension), never "code ✓". Usage: node scripts/classify-oia.mjs owner/name [issueNumber] [YYYY-MM-DD]
import { pathToFileURL } from 'node:url';
-import { LAYER_NAMES, buildCorpus, classifySignals } from '../docs/oia-signals.mjs';
+import { LAYER_NAMES, SCAN_CAP, buildCorpus, classifySignals } from '../docs/oia-signals.mjs';
const NAME_RE=/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/; // GitHub owner/repo charset
// Untrusted text (repo description) is later rendered via innerHTML on the page.
@@ -40,15 +40,26 @@ export async function classify(name,issue,submitted){
const gate={issues:!!meta.has_issues,docs:paths.some(x=>/^docs?\//.test(x))||readme.length>800,ADRs:paths.some(x=>/adrs?\/|adr-\d/.test(x)),PRD:paths.some(x=>/prd/.test(x)),infographic:paths.some(x=>/infographic|\.svg$/.test(x))};
const gateStr=Object.entries(gate).map(([k,v])=>`${k} ${v?'✓':'✗'}`).join(', ');
+ // #64: scaffolding exclusion and scan-cap truncation are reported separately.
+ // Summing them and calling the total "scaffolding" overstated the count by two
+ // orders of magnitude on large repos and hid how much of the tree went unread.
+ const denoisedTotal=corpus.kept+corpus.truncated;
+ const scanScope=corpus.truncated?`${corpus.kept} of ${denoisedTotal}`:`${corpus.kept}`;
+ const scanParts=[];
+ if(corpus.dropped) scanParts.push(`${corpus.dropped} scaffolding path${corpus.dropped===1?'':'s'} excluded`);
+ if(corpus.truncated) scanParts.push(`${corpus.truncated} beyond the ${SCAN_CAP}-file scan cap, not read`);
+ if(t.json.truncated) scanParts.push('tree truncated by GitHub');
+ const scanNote=scanParts.length?` (${scanParts.join('; ')})`:'';
+
return {
name,
desc:clean(meta.description||name,120),
layers,
spans,
notes,
- narrative:`Auto-classified from a submission. Heuristic file-tree scan of ${corpus.kept} files${t.json.truncated?' (tree truncated)':''}${corpus.dropped?` (${corpus.dropped} scaffolding paths excluded)`:''}; language ${meta.language||'n/a'}, pushed ${(meta.pushed_at||'').slice(0,10)}. Centre of gravity ${cog.join(', ')||'none detected'}; presence ${pres.join(', ')||'none'}. This is a structural signal pass, NOT a comprehension read, and awaits committee review before promotion.`,
+ narrative:`Auto-classified from a submission. Heuristic file-tree scan of ${scanScope} project files${scanNote}; language ${meta.language||'n/a'}, pushed ${(meta.pushed_at||'').slice(0,10)}. Centre of gravity ${cog.join(', ')||'none detected'}; presence ${pres.join(', ')||'none'}. This is a structural signal pass, NOT a comprehension read, and awaits committee review before promotion.`,
category:'app',
- evidence:{t:'auto',n:`Heuristic file-tree scan of ${corpus.kept} project files on ${submitted||'submission'} (${corpus.dropped} scaffolding paths excluded): structural signals (real files, not just names), not comprehension. Qualification gate: ${gateStr}. Promote to code ✓ with a reader-agent audit and committee vote.`},
+ evidence:{t:'auto',n:`Heuristic file-tree scan of ${scanScope} project files on ${submitted||'submission'}${scanNote}: structural signals (real files, not just names), not comprehension. Qualification gate: ${gateStr}. Promote to code ✓ with a reader-agent audit and committee vote.`},
gaps:['Committee: verify these auto-detected placements against the actual source (auto → code ✓).','Surface issue tracker + documentation + PRD/ADRs in the repo to meet the qualification gate.','Name the OIA layers/spans the project targets in its README.'],
status:'pending',
submitted:submitted||null,
diff --git a/test/oia-signals.test.js b/test/oia-signals.test.js
index 7e5ff96..bc53b20 100644
--- a/test/oia-signals.test.js
+++ b/test/oia-signals.test.js
@@ -13,7 +13,7 @@ import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
import {
- LSIG, SSIG, BOILERPLATE, denoisePaths, buildCorpus, classifySignals, cnt,
+ LSIG, SSIG, BOILERPLATE, SCAN_CAP, denoisePaths, buildCorpus, classifySignals, cnt,
} from '../docs/oia-signals.mjs';
const HERE = dirname(fileURLToPath(import.meta.url));
@@ -162,3 +162,43 @@ test('#50: the server classifier defines no signal maps of its own', () => {
assert.doesNotMatch(src, /const\s+LSIG\s*=/, 'classify-oia.mjs must not redeclare LSIG');
assert.doesNotMatch(src, /const\s+SSIG\s*=/, 'classify-oia.mjs must not redeclare SSIG');
});
+
+// ---------------------------------------------------------------------------
+// Issue #64 defect 1 — scaffolding and scan-cap truncation are different things
+// and must be counted separately.
+//
+// buildCorpus used to compute `dropped` against the pre-cap length, so the two
+// were summed and the narrative attributed all of it to scaffolding. On
+// future-agi/future-agi that published "6455 scaffolding paths excluded" when
+// 58 were scaffolding and 6397 were simply beyond the scan cap: an overstatement
+// of roughly 110x, and it concealed that only 48% of the repo was read.
+//
+// It stayed latent because every earlier submission was smaller than the cap,
+// so `dropped` was 0 and the parenthetical never rendered.
+// ---------------------------------------------------------------------------
+test('#64: dropped counts scaffolding only, truncated counts the scan-cap overflow', () => {
+ const scaffolding = ['.github/workflows/ci.yml', 'package-lock.json', 'license'];
+ const overflow = 500;
+ const real = Array.from({ length: SCAN_CAP + overflow }, (_, i) => `src/mod${i}.ts`);
+ const c = buildCorpus({ paths: [...scaffolding, ...real] });
+
+ assert.equal(c.dropped, scaffolding.length,
+ 'dropped must count de-noised scaffolding only, never the scan-cap overflow');
+ assert.equal(c.truncated, overflow,
+ 'truncated must count the paths discarded by the scan cap');
+ assert.equal(c.kept, SCAN_CAP, 'kept is capped at SCAN_CAP');
+});
+
+test('#64: a repo under the scan cap reports no truncation', () => {
+ const c = buildCorpus({ paths: ['.github/workflows/ci.yml', 'src/a.ts', 'src/b.ts'] });
+ assert.equal(c.dropped, 1, 'the workflow file is scaffolding');
+ assert.equal(c.truncated, 0, 'nothing was beyond the cap');
+ assert.equal(c.kept, 2);
+});
+
+test('#64: kept + dropped + truncated accounts for every input path', () => {
+ const paths = ['license', ...Array.from({ length: SCAN_CAP + 7 }, (_, i) => `pkg/f${i}.py`)];
+ const c = buildCorpus({ paths });
+ assert.equal(c.kept + c.dropped + c.truncated, paths.length,
+ 'the three counters must partition the input, with nothing unaccounted for');
+});