@mjasnikovs/pi-task 0.40.32 → 0.40.34

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/README.md CHANGED
@@ -268,7 +268,7 @@ bun run lint # prettier + eslint + tsc --noEmit
268
268
  bun run build # tsc → dist/
269
269
  ```
270
270
 
271
- Built with [Bun](https://bun.sh), TypeScript (strict), and [TypeBox](https://github.com/sinclairzx81/typebox) for tool schemas. Design plans live in [`plans/`](./plans).
271
+ Built with [Bun](https://bun.sh), TypeScript (strict), and [TypeBox](https://github.com/sinclairzx81/typebox) for tool schemas.
272
272
 
273
273
  ## License
274
274
 
@@ -65,7 +65,7 @@ export declare function verifyExcerpt(excerpt: string, content: string): Excerpt
65
65
  * fabrication. An excerpt assembled from several real spans is a stitched quote,
66
66
  * which is what the extraction prompt produces — and calling that a possible
67
67
  * hallucination was wrong on 21 of 21 measured cases, on a fifth of every run's
68
- * answers. See "Defect 18" in DOC_REGRESSINONS.md.
68
+ * answers.
69
69
  */
70
70
  export declare function formatResultText(header: string, parsed: {
71
71
  answer: string;
@@ -112,7 +112,7 @@ export function verifyExcerpt(excerpt, content) {
112
112
  * fabrication. An excerpt assembled from several real spans is a stitched quote,
113
113
  * which is what the extraction prompt produces — and calling that a possible
114
114
  * hallucination was wrong on 21 of 21 measured cases, on a fifth of every run's
115
- * answers. See "Defect 18" in DOC_REGRESSINONS.md.
115
+ * answers.
116
116
  */
117
117
  export function formatResultText(header, parsed, check) {
118
118
  if (!parsed.excerpt) {
@@ -70,7 +70,8 @@ export interface SessionRequest {
70
70
  failed: boolean;
71
71
  /** CDP resource type, collapsed: what ISSUED this request. */
72
72
  initiator: 'xhr' | 'document' | 'other';
73
- /** Relative to the sign-in request: before it, it, or after it. */
73
+ /** Before the submit, the sign-in request itself, or issued at or after the
74
+ * submit. */
74
75
  phase: 'pre' | 'auth' | 'post';
75
76
  }
76
77
  export interface DeepSessionFacts {
@@ -93,8 +94,8 @@ export interface DeepSessionFacts {
93
94
  status: number | null;
94
95
  failed: boolean;
95
96
  } | null;
96
- /** Same-origin XHR/fetch requests issued AFTER the sign-in response, excluding
97
- * the sign-in request itself. Derived: `sessionRequests` in phase 'post'. */
97
+ /** Same-origin XHR/fetch requests issued at or after the submit, excluding the
98
+ * sign-in request itself. Derived: `sessionRequests` in phase 'post'. */
98
99
  postAuthDataAttempted: number;
99
100
  postAuthData2xx: number;
100
101
  /** Origins the client called that are not the app's own, whose requests failed
@@ -112,8 +113,9 @@ export interface DeepSessionFacts {
112
113
  * The three request-shaped facts, computed from the log and from nothing else. The
113
114
  * driver records `sessionRequests` and calls this; the values are exactly what the
114
115
  * pre-log driver computed by filtering the same map (the sign-in request is the
115
- * first same-origin non-GET after submit; the data requests are the same-origin
116
- * XHR/fetch issued at or after it, itself excluded).
116
+ * first same-origin non-GET issued at or after the submit; the data requests are the
117
+ * same-origin
118
+ * XHR/fetch issued at or after the submit, the sign-in request itself excluded).
117
119
  */
118
120
  export declare function deriveLegacyFacts(log: SessionRequest[]): Pick<DeepSessionFacts, 'authRequest' | 'postAuthDataAttempted' | 'postAuthData2xx'>;
119
121
  /**
@@ -225,7 +227,7 @@ export interface DriveSessionOptions {
225
227
  /**
226
228
  * The session over an already-connected browser: navigate, inspect, sign in if the
227
229
  * landing is a wall and credentials exist, settle, phase the same-origin request
228
- * log against the sign-in request, re-enter once the sign-in was accepted, and
230
+ * log against the submit, re-enter once the sign-in was accepted, and
229
231
  * hand the facts to `judge`. Pure protocol logic — no process, no filesystem, no
230
232
  * socket — so every branch is testable against a fake `CdpLike`.
231
233
  *
@@ -202,8 +202,9 @@ export function pinnedLocalPort(vars) {
202
202
  * The three request-shaped facts, computed from the log and from nothing else. The
203
203
  * driver records `sessionRequests` and calls this; the values are exactly what the
204
204
  * pre-log driver computed by filtering the same map (the sign-in request is the
205
- * first same-origin non-GET after submit; the data requests are the same-origin
206
- * XHR/fetch issued at or after it, itself excluded).
205
+ * first same-origin non-GET issued at or after the submit; the data requests are the
206
+ * same-origin
207
+ * XHR/fetch issued at or after the submit, the sign-in request itself excluded).
207
208
  */
208
209
  export function deriveLegacyFacts(log) {
209
210
  const auth = log.find(r => r.phase === 'auth') ?? null;
@@ -636,7 +637,7 @@ export async function launchBrowser(bin, userDataDir, { signal } = {}) {
636
637
  /**
637
638
  * The session over an already-connected browser: navigate, inspect, sign in if the
638
639
  * landing is a wall and credentials exist, settle, phase the same-origin request
639
- * log against the sign-in request, re-enter once the sign-in was accepted, and
640
+ * log against the submit, re-enter once the sign-in was accepted, and
640
641
  * hand the facts to `judge`. Pure protocol logic — no process, no filesystem, no
641
642
  * socket — so every branch is testable against a fake `CdpLike`.
642
643
  *
@@ -645,18 +646,27 @@ export async function launchBrowser(bin, userDataDir, { signal } = {}) {
645
646
  export async function driveSession(cdp, { url, credentials, judge, quietMs }) {
646
647
  const origin = new URL(url).origin;
647
648
  const requests = new Map();
649
+ let nextSeq = 0;
648
650
  let lastActivity = Date.now();
649
651
  cdp.on('Network.requestWillBeSent', p => {
650
- const req = p.request;
651
- requests.set(String(p.requestId), {
652
- url: String(req?.url ?? ''),
653
- method: String(req?.method ?? 'GET'),
654
- type: String(p.type ?? ''),
655
- status: null,
656
- mimeType: null,
657
- failed: false,
658
- at: Date.now()
659
- });
652
+ const id = String(p.requestId);
653
+ // A redirect hop reuses the requestId, carrying the NEXT hop's method and
654
+ // url. The request the client made is the first hop and the status that
655
+ // judges it is the chain's last, so the first hop stays and the eventual
656
+ // response lands on it. Overwriting reads a POST that 302s as a GET, and
657
+ // then no sign-in request is ever found.
658
+ if (p.redirectResponse === undefined || !requests.has(id)) {
659
+ const req = p.request;
660
+ requests.set(id, {
661
+ url: String(req?.url ?? ''),
662
+ method: String(req?.method ?? 'GET'),
663
+ type: String(p.type ?? ''),
664
+ status: null,
665
+ mimeType: null,
666
+ failed: false,
667
+ seq: nextSeq++
668
+ });
669
+ }
660
670
  lastActivity = Date.now();
661
671
  });
662
672
  cdp.on('Network.responseReceived', p => {
@@ -711,27 +721,23 @@ export async function driveSession(cdp, { url, credentials, judge, quietMs }) {
711
721
  const initiatorOf = (r) => isData(r) ? 'xhr'
712
722
  : r.type === 'Document' ? 'document'
713
723
  : 'other';
714
- /** The same-origin request log, phased against the sign-in request. `authAt` is
715
- * Infinity before the submit, so every request so far is 'pre'. */
716
- const sessionLog = (authId, authAt) => {
717
- const out = [];
718
- for (const [id, r] of requests) {
719
- if (!sameOrigin(r))
720
- continue;
721
- out.push({
722
- method: r.method,
723
- path: pathOf(r.url),
724
- status: r.status,
725
- mimeType: r.mimeType,
726
- failed: r.failed,
727
- initiator: initiatorOf(r),
728
- phase: id === authId ? 'auth'
729
- : r.at >= authAt ? 'post'
730
- : 'pre'
731
- });
732
- }
733
- return out;
734
- };
724
+ /** The same-origin request log, phased against the submit. `postSeq` is the
725
+ * first `seq` the submit could issue Infinity while no submit has happened —
726
+ * so before a submit every request so far is 'pre'; the sign-in request sits at
727
+ * or after the boundary but is 'auth', not 'post'. */
728
+ const sessionLog = (authId, postSeq) => [...requests]
729
+ .filter(([, r]) => sameOrigin(r))
730
+ .map(([id, r]) => ({
731
+ method: r.method,
732
+ path: pathOf(r.url),
733
+ status: r.status,
734
+ mimeType: r.mimeType,
735
+ failed: r.failed,
736
+ initiator: initiatorOf(r),
737
+ phase: id === authId ? 'auth'
738
+ : r.seq >= postSeq ? 'post'
739
+ : 'pre'
740
+ }));
735
741
  const facts = (log, over) => ({
736
742
  sessionRequests: log,
737
743
  ...deriveLegacyFacts(log),
@@ -749,13 +755,15 @@ export async function driveSession(cdp, { url, credentials, judge, quietMs }) {
749
755
  const unsubmitted = (over) => facts(sessionLog(null, Number.POSITIVE_INFINITY), over);
750
756
  if (!before.hasPassword || credentials === null)
751
757
  return judge(unsubmitted({}));
752
- const submitMark = Date.now();
753
758
  const filled = await evaluate(fillExpr(credentials.identifier, credentials.password));
754
759
  if (!filled?.ok)
755
760
  return judge(unsubmitted({ submitted: false }));
756
761
  // Separate turn: the fill's input events schedule framework state updates that
757
762
  // the submit handler must already see.
758
763
  await sleep(300);
764
+ // Captured after the fill: background traffic before the submit must not be
765
+ // eligible as the sign-in request.
766
+ const submitSeq = nextSeq;
759
767
  const submitted = await evaluate(SUBMIT_EXPR);
760
768
  if (!submitted?.ok)
761
769
  return judge(unsubmitted({ submitted: false }));
@@ -765,16 +773,14 @@ export async function driveSession(cdp, { url, credentials, judge, quietMs }) {
765
773
  // own 2xx is the precondition for judging anything, and it is EXCLUDED from the
766
774
  // data evidence — a broken build satisfies "at least one same-origin 2xx" with
767
775
  // exactly this request and nothing else.
768
- const after = new Map([...requests].filter(([, r]) => r.at >= submitMark));
769
776
  let authId = null;
770
- for (const [id, r] of after) {
771
- if (sameOrigin(r) && r.method !== 'GET') {
777
+ for (const [id, r] of requests) {
778
+ if (r.seq >= submitSeq && sameOrigin(r) && r.method !== 'GET') {
772
779
  authId = id;
773
780
  break;
774
781
  }
775
782
  }
776
- const authReq = authId !== null ? after.get(authId) : null;
777
- const authAt = authReq?.at ?? submitMark;
783
+ const authReq = authId === null ? null : requests.get(authId);
778
784
  const now = await evaluate(INSPECT_EXPR);
779
785
  const domJudgment = judgeRenderedDom(now?.html ?? '');
780
786
  const leftAuthWall = !(now?.hasPassword ?? false) || (now?.pathname ?? '') !== before.pathname;
@@ -795,7 +801,7 @@ export async function driveSession(cdp, { url, credentials, judge, quietMs }) {
795
801
  lastActivity = Date.now();
796
802
  await settle(() => lastActivity, RE_NAV_CAP_MS, quietMs);
797
803
  }
798
- return judge(facts(sessionLog(authId, authAt), {
804
+ return judge(facts(sessionLog(authId, submitSeq), {
799
805
  submitted: true,
800
806
  foreignOriginFailures: foreignOriginFailures(),
801
807
  leftAuthWall,
@@ -549,13 +549,11 @@ function docsRawCached(cache, pkg, profile, query, ensureIndexed, retrieveChunks
549
549
  }
550
550
  function docsRawUncached(pkg, profile, cacheError, autoInstalled) {
551
551
  const parts = [];
552
+ // Already entry-first, and already past the selection rules: assembling that
553
+ // order here needed the entry compared against a list the walker spells with
554
+ // its own resolved paths, and a miss read as "a rule dropped it".
552
555
  const surfaceFiles = collectFiles(pkg, profile).surface;
553
- // Only an entry the rules KEPT goes first. A CJS-first package names its
554
- // `.d.cts` in `types`, so the entry is exactly the twin `collectFiles` drops,
555
- // and prepending it blind put it back at the head of the truncated blob.
556
- const entry = pkg.entry !== null && surfaceFiles.includes(pkg.entry) ? pkg.entry : null;
557
- const entryFirst = entry === null ? surfaceFiles : [entry, ...surfaceFiles.filter(f => f !== entry)];
558
- for (const abs of entryFirst) {
556
+ for (const abs of surfaceFiles) {
559
557
  let raw;
560
558
  try {
561
559
  raw = fs.readFileSync(abs, 'utf8');
@@ -87,7 +87,7 @@ export interface EcosystemProfile {
87
87
  /**
88
88
  * Packages whose declarations belong in THIS package's index, because this
89
89
  * package exports names it does not declare — `hspec`/`hspec-core`,
90
- * `axum`/`axum-core`; see DEFECT-12-STOPPING-RULE.md.
90
+ * `axum`/`axum-core`.
91
91
  */
92
92
  supplements?: (pkg: ResolvedPackage, cwd: string, io: EcosystemIo) => Promise<ResolvedPackage[]>;
93
93
  /**
@@ -58,9 +58,14 @@ function computeContentHash(pkg, profile, supplements = []) {
58
58
  hash.update(Buffer.from(chunkerFingerprint(), 'utf8'));
59
59
  hash.update(ZERO_SEP);
60
60
  // Source text, the same trick as `declSplitRe.source`: the fingerprint moves
61
- // whenever the selection rule does, with nothing to remember to bump.
61
+ // whenever the selection rule does, with nothing to remember to bump. The
62
+ // walker is here because it decides which files EXIST to be selected, and it
63
+ // sits one level below `String(ingestBody)` — the level three earlier fixes
64
+ // hid in.
62
65
  hash.update(Buffer.from(`${String(profile.isSurfaceFile)}\u0000${String(profile.selectFiles)}`
63
- + `\u0000${String(dropDeadMajors)}`, 'utf8'));
66
+ + `\u0000${String(dropDeadMajors)}\u0000${String(walkSurface)}`
67
+ + `\u0000${String(withinPackage)}\u0000${String(entryFirst)}`
68
+ + `\u0000${profile.skipDirs.join(',')}`, 'utf8'));
64
69
  hash.update(ZERO_SEP);
65
70
  // The extractor and the writer, by source. Surfacing only `pkg.entry` below
66
71
  // leaves a package cached whenever a fix moves some OTHER module — the
@@ -91,7 +96,9 @@ function computeContentHash(pkg, profile, supplements = []) {
91
96
  return hash.digest('hex');
92
97
  }
93
98
  function walkSurface(root, profile) {
94
- const out = [];
99
+ // A set, not a list: two links to one file are one file, and the uncached
100
+ // path prints this list into a single already-truncated blob.
101
+ const out = new Set();
95
102
  const stack = [root];
96
103
  // A directory symlink is followed by its resolved target, so one pointing at
97
104
  // an ancestor inside `root` walks the same subtree forever without this.
@@ -119,17 +126,19 @@ function walkSurface(root, profile) {
119
126
  catch {
120
127
  continue;
121
128
  }
122
- const relReal = path.relative(root, realPath);
123
- if (relReal.startsWith('..'))
129
+ if (!withinPackage(root, realPath, profile))
124
130
  continue;
125
131
  if (stat.isDirectory()) {
126
132
  if (walked.has(realPath))
127
133
  continue;
128
134
  walked.add(realPath);
129
135
  stack.push(realPath);
136
+ // Surface-ness is the visible name's, identity is the target's:
137
+ // `index.d.ts -> src/impl.ts` is what a consumer imports, and
138
+ // `helpers_test.go -> helpers.go` is still a test file.
130
139
  }
131
- else if (stat.isFile() && profile.isSurfaceFile(realPath))
132
- out.push(realPath);
140
+ else if (stat.isFile() && profile.isSurfaceFile(entry.name))
141
+ out.add(realPath);
133
142
  continue;
134
143
  }
135
144
  if (entry.isDirectory()) {
@@ -139,10 +148,23 @@ function walkSurface(root, profile) {
139
148
  stack.push(full);
140
149
  }
141
150
  else if (entry.isFile() && profile.isSurfaceFile(entry.name))
142
- out.push(full);
151
+ out.add(full);
143
152
  }
144
153
  }
145
- return out.sort();
154
+ return [...out].sort();
155
+ }
156
+ /**
157
+ * Is this symlink target part of the package, by the same rules its own tree obeys?
158
+ *
159
+ * The link's NAME cleared `skipDirs`; the path it resolves to has to as well, or
160
+ * `deps -> node_modules` files another package's declarations under this one's
161
+ * name and version banner.
162
+ */
163
+ function withinPackage(root, realPath, profile) {
164
+ const rel = path.relative(root, realPath);
165
+ if (rel.startsWith('..'))
166
+ return false;
167
+ return !rel.split(path.sep).some(seg => profile.skipDirs.includes(seg));
146
168
  }
147
169
  /**
148
170
  * Drop a `.d.cts` / `.d.mts` that sits beside a `.d.ts` of the same name.
@@ -200,10 +222,48 @@ function dropDeadMajors(files, root, version) {
200
222
  export function collectFiles(pkg, profile) {
201
223
  const walked = walkSurface(pkg.root, profile);
202
224
  const surface = dropDeadMajors(walked, pkg.root, pkg.version);
203
- return {
204
- surface: profile.selectFiles ? profile.selectFiles(surface, pkg) : surface,
205
- readme: pkg.readme
206
- };
225
+ const selected = profile.selectFiles ? profile.selectFiles(surface, pkg) : surface;
226
+ return { surface: entryFirst(selected, walked, pkg), readme: pkg.readme };
227
+ }
228
+ /** The list's own spelling of `entry`, resolving links only if the plain compare misses. */
229
+ function sameFile(files, entry) {
230
+ const direct = files.find(f => f === entry);
231
+ if (direct !== undefined)
232
+ return direct;
233
+ const real = realpathOr(entry);
234
+ return files.find(f => f === real || realpathOr(f) === real) ?? null;
235
+ }
236
+ function realpathOr(file) {
237
+ try {
238
+ return fs.realpathSync(file);
239
+ }
240
+ catch {
241
+ return file;
242
+ }
243
+ }
244
+ /**
245
+ * The manifest's entry at the head, and back in the list when the walk never
246
+ * offered it.
247
+ *
248
+ * Two different questions, and membership alone cannot tell them apart. A rule
249
+ * that DROPPED the entry is doing its job — a CJS-first package names the
250
+ * `.d.cts` twin `dropParallelDeclarations` removes, and putting it back blind
251
+ * put it at the head of the truncated blob. A package naming a `src/index.ts`
252
+ * in `types` was never a surface-file candidate at all, and answering "has no
253
+ * .d.ts files" for it is not the same as answering without one file.
254
+ *
255
+ * The head matters on the uncached path, which prints this list into one
256
+ * truncated blob and nothing else.
257
+ */
258
+ function entryFirst(selected, walked, pkg) {
259
+ if (pkg.entry === null)
260
+ return [...selected];
261
+ const kept = sameFile(selected, pkg.entry);
262
+ if (kept !== null)
263
+ return [kept, ...selected.filter(f => f !== kept)];
264
+ if (sameFile(walked, pkg.entry) !== null)
265
+ return [...selected];
266
+ return fs.existsSync(pkg.entry) ? [pkg.entry, ...selected] : [...selected];
207
267
  }
208
268
  function ingestBody(cache, pkg, profile, contentHash, supplements = []) {
209
269
  const ecosystem = profile.id;
@@ -252,8 +312,9 @@ function ingestBody(cache, pkg, profile, contentHash, supplements = []) {
252
312
  }
253
313
  }
254
314
  // A facade package indexes to a table of contents: `hspec` is 14 chunks of
255
- // export lists and every signature is in `hspec-core`. Fill only the holes
256
- // see DEFECT-12-STOPPING-RULE.md for the boundary and why it stops here.
315
+ // export lists and every signature is in `hspec-core`. Fill only the holes,
316
+ // and only one hop out: after one hop hspec has zero unresolved names left,
317
+ // and no package measured had anything for a second hop to fetch.
257
318
  const found = supplements.length > 0 ? (profile.exportGap?.(pkg.root) ?? null) : null;
258
319
  const gap = found !== null && !found.empty ? found : null;
259
320
  for (const sup of gap === null ? [] : supplements) {
@@ -122,7 +122,7 @@ export declare function manifestCrates(cwd: string): Set<string> | undefined;
122
122
  * The trigger is the hole, with no threshold — measured, and for the same reason
123
123
  * as hackage: across twenty-two crates the unresolved fraction reads 100% on a
124
124
  * crate with one re-export and 0% on a crate with none, so a ratio separates
125
- * nothing. See "Defect 16" in DOC_REGRESSINONS.md for the sweep.
125
+ * nothing.
126
126
  */
127
127
  export declare function cargoExportGap(root: string): ExportGap;
128
128
  /**
@@ -869,7 +869,7 @@ export function manifestCrates(cwd) {
869
869
  }
870
870
  return out;
871
871
  }
872
- // ── the facade gap (DEFECT-12-STOPPING-RULE.md, cargo half) ─────────────────
872
+ // ── the facade gap, cargo half ──────────────────────────────────────────────
873
873
  /** A `pub use …;` statement, attributes and line breaks included. */
874
874
  const PUB_USE_RE = /\bpub\s+use\s+([^;]+);/g;
875
875
  /** Every item head that introduces a name, visibility ignored — a facade may
@@ -995,7 +995,7 @@ function moduleOfPath(relPath) {
995
995
  * The trigger is the hole, with no threshold — measured, and for the same reason
996
996
  * as hackage: across twenty-two crates the unresolved fraction reads 100% on a
997
997
  * crate with one re-export and 0% on a crate with none, so a ratio separates
998
- * nothing. See "Defect 16" in DOC_REGRESSINONS.md for the sweep.
998
+ * nothing.
999
999
  */
1000
1000
  export function cargoExportGap(root) {
1001
1001
  const deps = runtimeDeps(root);
@@ -25,7 +25,7 @@ import * as os from 'node:os';
25
25
  import * as path from 'node:path';
26
26
  import { ResolveError } from './docs-resolve.js';
27
27
  import { findAtOrAbove } from './eco-cargo.js';
28
- import { buildConstraint, splitGoItems } from './go-surface.js';
28
+ import { buildConstraint, codeOnly, splitGoItems } from './go-surface.js';
29
29
  import { readZip, readEntry, isUnsafeEntryName } from '../shared/zip.js';
30
30
  import { acquireStdlibPackage, findInGoroot, findSliced } from './go-stdlib.js';
31
31
  const PROXY = 'https://proxy.golang.org';
@@ -658,16 +658,22 @@ function holdsByDefault(src) {
658
658
  * line, and reading those keeps the very subpackage `selectOwnPackage` exists to
659
659
  * drop. A regex cannot draw that line — a column-0 `import (` also sits inside a
660
660
  * generator template's raw string and inside a doc comment's example — so the
661
- * split is the surface scanner's, which already skips comments and literals.
661
+ * split is the surface scanner's. That split strips only the comments BETWEEN
662
+ * declarations, and a commented-out import line inside the block is ordinary Go,
663
+ * so the item's own comments come off too.
662
664
  * An `ImportPath` is a `string_lit`, so the backtick form is legal Go and
663
665
  * dropping it silently drops everything reachable only through it.
664
666
  */
665
667
  function importsOf(src) {
666
668
  const out = [];
667
669
  for (const item of splitGoItems(src)) {
668
- if (!/^import\b/.test(item.text))
670
+ if (/^package\b/.test(item.text))
669
671
  continue;
670
- for (const m of item.text.matchAll(/"([^"\n]+)"|`([^`]+)`/g))
672
+ // Go puts every import declaration before every other one, so the first
673
+ // item that is not an import ends the search.
674
+ if (!/^import\b/.test(item.text))
675
+ break;
676
+ for (const m of codeOnly(item.text).matchAll(/"([^"\n]+)"|`([^`]+)`/g))
671
677
  out.push(m[1] ?? m[2]);
672
678
  }
673
679
  return out;
@@ -785,6 +791,7 @@ export function goContentFingerprintParts() {
785
791
  String(selectOwnPackage),
786
792
  String(declaresApi),
787
793
  String(goMajor),
788
- String(importsOf)
794
+ String(importsOf),
795
+ String(codeOnly)
789
796
  ];
790
797
  }
@@ -600,8 +600,9 @@ export function manifestPackages(cwd) {
600
600
  * re-export puts the name in the export list; a `module X` re-export puts
601
601
  * nothing there at all, which is why `shouldBe` is invisible to the first.
602
602
  *
603
- * See DEFECT-12-STOPPING-RULE.md for why this triggers on the hole itself
604
- * rather than on a fraction of the export list.
603
+ * The trigger is the hole, with no threshold. Swept over the 299 modules with
604
+ * 5+ exports, the trigger count reads 33 at 50% and 18 at 95%, so the fraction
605
+ * is not carrying the decision and any value picked would just sound right.
605
606
  */
606
607
  const EXPORT_NAME_RE = /^[A-Za-z_][\w']*$/;
607
608
  /** `module X` inside an export list is a re-export; `Prelude` is base, and base is not fetched. */
@@ -4,8 +4,8 @@
4
4
  * `hspec` indexes to a table of contents and every signature is in `hspec-core`;
5
5
  * `axum` re-exports `IntoResponse` and the trait lives in `axum-core`. Both are
6
6
  * the same failure — a query retrieves the package's own chunks and not one of
7
- * them defines the thing asked about — and DEFECT-12-STOPPING-RULE.md fixes the
8
- * boundary for following the re-export.
7
+ * them defines the thing asked about — so the re-export is followed exactly one
8
+ * hop, to a dependency the package's own name prefixes.
9
9
  *
10
10
  * The boundary is shared; the parsing is not. Haskell states the gap in an export
11
11
  * list, Rust in `pub use`, so each ecosystem answers the same three questions in
@@ -38,6 +38,15 @@ export interface GoItem {
38
38
  pending: string;
39
39
  text: string;
40
40
  }
41
+ /**
42
+ * A declaration's text with its comments removed and its literals intact.
43
+ *
44
+ * `splitGoItems` strips only what sits BETWEEN declarations. A comment inside an
45
+ * import block, or trailing one, stays in the item's own text, so a caller
46
+ * reading string literals out of a declaration reads the commented-out ones too
47
+ * — and a commented-out import is one of the commonest shapes in Go source.
48
+ */
49
+ export declare function codeOnly(text: string): string;
41
50
  /**
42
51
  * Split source into declarations. Works unchanged on a struct body or a const
43
52
  * group, whose members obey the same semicolon rule with no keyword in front.
@@ -162,6 +162,39 @@ function endsStatement(word, lastChar) {
162
162
  return !CONTINUING_KEYWORDS.has(word);
163
163
  return STMT_END_CHAR_RE.test(lastChar);
164
164
  }
165
+ /**
166
+ * A declaration's text with its comments removed and its literals intact.
167
+ *
168
+ * `splitGoItems` strips only what sits BETWEEN declarations. A comment inside an
169
+ * import block, or trailing one, stays in the item's own text, so a caller
170
+ * reading string literals out of a declaration reads the commented-out ones too
171
+ * — and a commented-out import is one of the commonest shapes in Go source.
172
+ */
173
+ export function codeOnly(text) {
174
+ let out = '';
175
+ let i = 0;
176
+ while (i < text.length) {
177
+ const c = text[i];
178
+ if (c === '/' && text[i + 1] === '/') {
179
+ const nl = text.indexOf('\n', i);
180
+ i = nl < 0 ? text.length : nl;
181
+ }
182
+ else if (c === '/' && text[i + 1] === '*') {
183
+ const close = text.indexOf('*/', i + 2);
184
+ i = close < 0 ? text.length : close + 2;
185
+ }
186
+ else if (c === '"' || c === "'" || c === '`') {
187
+ const end = skipLiteral(text, i);
188
+ out += text.slice(i, end);
189
+ i = end;
190
+ }
191
+ else {
192
+ out += c;
193
+ i++;
194
+ }
195
+ }
196
+ return out;
197
+ }
165
198
  /**
166
199
  * Split source into declarations. Works unchanged on a struct body or a const
167
200
  * group, whose members obey the same semicolon rule with no keyword in front.
@@ -530,6 +563,7 @@ export function goContentFingerprint() {
530
563
  endsStatement,
531
564
  skipToCode,
532
565
  skipLiteral,
566
+ codeOnly,
533
567
  trailingGroup,
534
568
  splitBody,
535
569
  buildConstraint,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mjasnikovs/pi-task",
3
- "version": "0.40.32",
3
+ "version": "0.40.34",
4
4
  "description": "Deterministic task planning and spec-orchestration for local models — crash-safe /task pipelines with verify/enforce gates, a real-time remote web view, and web/docs/fetch/worker subagent tools.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",