truecourse 0.7.0-next.6 → 0.7.0-next.8

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/cli.mjs CHANGED
@@ -171176,7 +171176,7 @@ function readToolVersion() {
171176
171176
  if (cachedVersion)
171177
171177
  return cachedVersion;
171178
171178
  if (true) {
171179
- cachedVersion = "0.7.0-next.6";
171179
+ cachedVersion = "0.7.0-next.8";
171180
171180
  return cachedVersion;
171181
171181
  }
171182
171182
  try {
@@ -197210,7 +197210,17 @@ var OverlapSectionSchema = external_exports.object({
197210
197210
  * from older corpora still parses; `null` is the preamble marker the viewer
197211
197211
  * bands as the pre-first-heading block.
197212
197212
  */
197213
- heading: external_exports.string().nullable()
197213
+ heading: external_exports.string().nullable(),
197214
+ /**
197215
+ * A short verbatim excerpt (≤ ~25 words) of the disputed sentence, copied from
197216
+ * the doc — the model's evidence for the heading it picked. Persisted (optional,
197217
+ * so older corpora without it still parse) for verification transparency and so
197218
+ * the viewer can later highlight the exact disputed sentence, not just band the
197219
+ * section. Consumed at assembly by `verifyOverlapSections` to anchor the pointer
197220
+ * by exact location; NOT part of the cross-area dedup identity (that stays
197221
+ * doc + heading).
197222
+ */
197223
+ quote: external_exports.string().optional()
197214
197224
  });
197215
197225
  var OverlapSchema = external_exports.object({
197216
197226
  /** The two docs that overlap, by ref. */
@@ -198003,6 +198013,237 @@ import { createHash as createHash7 } from "node:crypto";
198003
198013
  import fs26 from "node:fs";
198004
198014
  init_transport();
198005
198015
  init_dist5();
198016
+
198017
+ // packages/spec-consolidator/dist/pointer-verifier.js
198018
+ var MIN_MEANINGFUL_SCORE = 1.5;
198019
+ var NEGLIGIBLE_RATIO = 0.25;
198020
+ var STOPWORDS2 = /* @__PURE__ */ new Set([
198021
+ "the",
198022
+ "a",
198023
+ "an",
198024
+ "and",
198025
+ "or",
198026
+ "but",
198027
+ "if",
198028
+ "then",
198029
+ "else",
198030
+ "of",
198031
+ "to",
198032
+ "in",
198033
+ "on",
198034
+ "at",
198035
+ "by",
198036
+ "for",
198037
+ "with",
198038
+ "as",
198039
+ "is",
198040
+ "are",
198041
+ "was",
198042
+ "were",
198043
+ "be",
198044
+ "been",
198045
+ "being",
198046
+ "it",
198047
+ "its",
198048
+ "this",
198049
+ "that",
198050
+ "these",
198051
+ "those",
198052
+ "they",
198053
+ "them",
198054
+ "their",
198055
+ "there",
198056
+ "here",
198057
+ "no",
198058
+ "not",
198059
+ "yes",
198060
+ "so",
198061
+ "than",
198062
+ "too",
198063
+ "very",
198064
+ "can",
198065
+ "could",
198066
+ "will",
198067
+ "would",
198068
+ "shall",
198069
+ "should",
198070
+ "may",
198071
+ "might",
198072
+ "must",
198073
+ "do",
198074
+ "does",
198075
+ "did",
198076
+ "done",
198077
+ "has",
198078
+ "have",
198079
+ "had",
198080
+ "from",
198081
+ "up",
198082
+ "out",
198083
+ "down",
198084
+ "over",
198085
+ "under",
198086
+ "again",
198087
+ "we",
198088
+ "you",
198089
+ "your",
198090
+ "our",
198091
+ "us",
198092
+ "i",
198093
+ "he",
198094
+ "she",
198095
+ "his",
198096
+ "her",
198097
+ "each",
198098
+ "any",
198099
+ "all",
198100
+ "both",
198101
+ "some",
198102
+ "such",
198103
+ "only",
198104
+ "own",
198105
+ "same",
198106
+ "more",
198107
+ "most",
198108
+ "other",
198109
+ "into",
198110
+ "about",
198111
+ "when",
198112
+ "where",
198113
+ "which",
198114
+ "who",
198115
+ "whom",
198116
+ "what",
198117
+ "how",
198118
+ "why",
198119
+ "per",
198120
+ "via",
198121
+ "also"
198122
+ ]);
198123
+ function tokenize(text4, drop) {
198124
+ const lowered = text4.toLowerCase().replace(/[`*_~#>]/g, " ");
198125
+ const out = [];
198126
+ for (const raw of lowered.split(/[^a-z0-9]+/)) {
198127
+ if (raw.length < 2)
198128
+ continue;
198129
+ if (STOPWORDS2.has(raw))
198130
+ continue;
198131
+ if (drop.has(raw))
198132
+ continue;
198133
+ out.push(raw);
198134
+ }
198135
+ return out;
198136
+ }
198137
+ function pathWords(p2) {
198138
+ return p2.toLowerCase().split(/[^a-z0-9]+/).filter((t2) => t2.length >= 2);
198139
+ }
198140
+ var HEADING_RE = /^ {0,3}#{1,6}\s+(.*)$/;
198141
+ function splitDocSections(body, drop) {
198142
+ const raws = [];
198143
+ let cur = { heading: "", text: "" };
198144
+ for (const line of body.split(/\r?\n/)) {
198145
+ const m = HEADING_RE.exec(line);
198146
+ if (m) {
198147
+ if (cur.text.trim() || cur.heading)
198148
+ raws.push(cur);
198149
+ cur = { heading: m[1].trim(), text: `${line}
198150
+ ` };
198151
+ } else {
198152
+ cur.text += `${line}
198153
+ `;
198154
+ }
198155
+ }
198156
+ if (cur.text.trim() || cur.heading)
198157
+ raws.push(cur);
198158
+ return raws.map((r) => ({
198159
+ realHeading: r.heading === "" ? null : r.heading,
198160
+ tokens: new Set(tokenize(r.text, drop)),
198161
+ text: r.text
198162
+ }));
198163
+ }
198164
+ function headingKey(h) {
198165
+ return h.replace(/[`*_~]/g, "").trim().toLowerCase();
198166
+ }
198167
+ function normalizeForQuote(text4) {
198168
+ return text4.replace(/[`*_~]/g, "").toLowerCase().replace(/\s+/g, " ").trim();
198169
+ }
198170
+ function locateQuote(sections, quote) {
198171
+ const needle = normalizeForQuote(quote);
198172
+ if (!needle)
198173
+ return [];
198174
+ const hits = [];
198175
+ for (let i = 0; i < sections.length; i++) {
198176
+ if (normalizeForQuote(sections[i].text).includes(needle))
198177
+ hits.push(i);
198178
+ }
198179
+ return hits;
198180
+ }
198181
+ function scoreSections(sections, noteTokens) {
198182
+ const N = sections.length;
198183
+ const df = /* @__PURE__ */ new Map();
198184
+ for (const s of sections)
198185
+ for (const t2 of s.tokens)
198186
+ df.set(t2, (df.get(t2) ?? 0) + 1);
198187
+ const idf = (t2) => {
198188
+ const d3 = df.get(t2) ?? 0;
198189
+ if (d3 === 0)
198190
+ return 0;
198191
+ return Math.max(0, Math.log2((N + 1) / (d3 + 1)));
198192
+ };
198193
+ return sections.map((s) => {
198194
+ let score = 0;
198195
+ for (const t2 of noteTokens)
198196
+ if (s.tokens.has(t2))
198197
+ score += idf(t2);
198198
+ return score;
198199
+ });
198200
+ }
198201
+ function verifyOverlapSections(input) {
198202
+ const { docs, note, sections, bodyOf } = input;
198203
+ if (sections.length === 0)
198204
+ return [...sections];
198205
+ const drop = /* @__PURE__ */ new Set([...pathWords(docs[0]), ...pathWords(docs[1])]);
198206
+ const noteTokens = new Set(tokenize(note, drop));
198207
+ return sections.map((ptr) => {
198208
+ const body = bodyOf(ptr.doc);
198209
+ if (body === void 0)
198210
+ return { ...ptr };
198211
+ const candidates = splitDocSections(body, drop);
198212
+ if (candidates.length === 0)
198213
+ return { ...ptr };
198214
+ let pointedIdx;
198215
+ if (ptr.heading === null) {
198216
+ pointedIdx = 0;
198217
+ } else {
198218
+ const key4 = headingKey(ptr.heading);
198219
+ pointedIdx = candidates.findIndex((c2) => c2.realHeading !== null && headingKey(c2.realHeading) === key4);
198220
+ }
198221
+ if (ptr.quote !== void 0) {
198222
+ const hits = locateQuote(candidates, ptr.quote);
198223
+ if (hits.length > 0) {
198224
+ if (pointedIdx >= 0 && hits.includes(pointedIdx))
198225
+ return { ...ptr };
198226
+ const target = hits[0];
198227
+ return { ...ptr, heading: target === 0 ? null : candidates[target].realHeading };
198228
+ }
198229
+ }
198230
+ if (noteTokens.size === 0)
198231
+ return { ...ptr };
198232
+ const scores = scoreSections(candidates, noteTokens);
198233
+ let bestIdx = 0;
198234
+ for (let i = 1; i < scores.length; i++)
198235
+ if (scores[i] > scores[bestIdx])
198236
+ bestIdx = i;
198237
+ const bestScore = scores[bestIdx];
198238
+ const pointedScore = pointedIdx >= 0 ? scores[pointedIdx] : 0;
198239
+ const reanchor = bestIdx !== pointedIdx && bestScore >= MIN_MEANINGFUL_SCORE && pointedScore < MIN_MEANINGFUL_SCORE && pointedScore <= NEGLIGIBLE_RATIO * bestScore;
198240
+ if (!reanchor)
198241
+ return { ...ptr };
198242
+ return { ...ptr, heading: bestIdx === 0 ? null : candidates[bestIdx].realHeading };
198243
+ });
198244
+ }
198245
+
198246
+ // packages/spec-consolidator/dist/overlap-detector.js
198006
198247
  var DEFAULT_MAX_PAIRS_PER_AREA = 60;
198007
198248
  async function flagOverlaps(repoRoot5, areas, docs, opts = {}) {
198008
198249
  const result = /* @__PURE__ */ new Map();
@@ -198092,6 +198333,19 @@ async function flagOverlaps(repoRoot5, areas, docs, opts = {}) {
198092
198333
  };
198093
198334
  launch();
198094
198335
  });
198336
+ for (const list of result.values()) {
198337
+ for (const overlap of list) {
198338
+ overlap.sections = verifyOverlapSections({
198339
+ docs: overlap.docs,
198340
+ note: overlap.note,
198341
+ sections: overlap.sections,
198342
+ bodyOf: (ref) => {
198343
+ const d3 = byPath.get(ref);
198344
+ return d3 ? docBody2(d3) : void 0;
198345
+ }
198346
+ });
198347
+ }
198348
+ }
198095
198349
  const entries = [...result].flatMap(([area, list]) => list.map((overlap) => ({ area, overlap })));
198096
198350
  const merged = dedupeCrossAreaOverlaps(entries);
198097
198351
  result.clear();
@@ -198146,9 +198400,12 @@ NOT a disagreement (do NOT flag):
198146
198400
 
198147
198401
  Bias: when there is a PLAUSIBLE contradiction a human should check, flag it. When the docs are clearly complementary or agree, do not.
198148
198402
 
198149
- When you flag an overlap, also identify the SECTIONS that conflict: the nearest markdown heading (the \`#\`/\`##\`/\`###\` title, verbatim, WITHOUT the leading #s) above the conflicting text in EACH doc. List one entry per side; omit a side only when it genuinely has no conflicting passage.
198403
+ When you flag an overlap, point at WHERE each side's disputed claim lives. Below each doc you are given a CLOSED list of that doc's section options \u2014 its headings, plus a "lead" option. Do NOT recall or guess a heading; SELECT from the list. For EACH side output a pointer with two fields:
198404
+ - \`heading\`: EXACTLY one of that doc's listed section headings, copied verbatim \u2014 OR the JSON literal \`null\` (not the string "null", not "") for the LEAD, the text ABOVE its first heading (or, when the doc opens straight with a title, that opening title block). Never emit a heading that is not one of the listed options.
198405
+ - \`quote\`: a SHORT verbatim excerpt (\u2264 25 words) of the disputed sentence, copied EXACTLY from that doc \u2014 the words that state the claim in dispute. This is your evidence for the heading you picked; copy it, do not paraphrase.
198406
+ List one entry per side; omit a side only when it genuinely has no conflicting passage.
198150
198407
 
198151
- PREAMBLE: when a doc's conflicting passage sits ABOVE its first \`#\` heading \u2014 the preamble block (a README's badges, tagline, or the intro line before any heading) \u2014 there is no heading to name. Set that side's \`heading\` to the JSON literal \`null\` (not the string "null", not ""). Use \`null\` ONLY for that pre-first-heading block; whenever the passage is under a heading, name the heading verbatim.
198408
+ PREAMBLE: use \`heading\`: \`null\` ONLY for that lead/preamble block; whenever the disputed passage is under a listed heading, select that heading verbatim.
198152
198409
 
198153
198410
  In the NOTE, refer to each doc by its FILENAME (the basename shown in the header, e.g. \`users.md\`) \u2014 NEVER "doc A" / "doc B", which mean nothing to the reader.
198154
198411
 
@@ -198156,13 +198413,19 @@ Output ONLY a JSON object, no prose, no code fences:
198156
198413
 
198157
198414
  { "overlap": true,
198158
198415
  "note": "users.md uses auth0_id; identity.md uses auth0_sub for the same user column",
198159
- "sections": [ { "side": "A", "heading": "User model" }, { "side": "B", "heading": "Identity" } ] }
198416
+ "sections": [
198417
+ { "side": "A", "heading": "User model", "quote": "the auth0_id column stores the Auth0 subject" },
198418
+ { "side": "B", "heading": "Identity", "quote": "we persist the Auth0 subject in auth0_sub" }
198419
+ ] }
198160
198420
 
198161
- Preamble example \u2014 doc A's claim is a README badge/tagline above any heading, so its side uses null:
198421
+ Preamble example \u2014 doc A's disputed claim is a README tagline ABOVE its first heading, so its side uses \`heading\`: null and quotes that tagline verbatim:
198162
198422
 
198163
198423
  { "overlap": true,
198164
198424
  "note": "README.md lists C# as a supported language in the preamble; plan.md's Tech Stack omits it",
198165
- "sections": [ { "side": "A", "heading": null }, { "side": "B", "heading": "Tech Stack" } ] }
198425
+ "sections": [
198426
+ { "side": "A", "heading": null, "quote": "Supports TypeScript, Python, and C# out of the box" },
198427
+ { "side": "B", "heading": "Tech Stack", "quote": "The stack is TypeScript and Python" }
198428
+ ] }
198166
198429
 
198167
198430
  Use { "overlap": false, "note": "", "sections": [] } when they are complementary or agree. The note is shown to the user \u2014 name the specific thing that differs.`;
198168
198431
  var OVERLAP_PREVIEW_LINES = 120;
@@ -198196,31 +198459,59 @@ function hasConcernHeading(doc, concern, vocab) {
198196
198459
  }
198197
198460
  return false;
198198
198461
  }
198462
+ function sectionHeadings(body) {
198463
+ const out = [];
198464
+ for (const line of body.split(/\r?\n/)) {
198465
+ const m = /^ {0,3}#{1,6}\s+(.*)$/.exec(line);
198466
+ if (!m)
198467
+ continue;
198468
+ const text4 = m[1].replace(/\s*#*\s*$/, "").trim();
198469
+ if (text4)
198470
+ out.push(text4);
198471
+ }
198472
+ return out;
198473
+ }
198474
+ function sectionOptions(slice5) {
198475
+ const lines = [" - the lead (text above the first heading, or the opening title block) \u2192 use heading: null"];
198476
+ for (const h of sectionHeadings(slice5))
198477
+ lines.push(` - ${h}`);
198478
+ return lines;
198479
+ }
198199
198480
  function buildOverlapUserPrompt(areaId, a, b) {
198200
198481
  const slice5 = (d3) => docBody2(d3).split(/\r?\n/).slice(0, OVERLAP_PREVIEW_LINES).join("\n");
198482
+ const sliceA = slice5(a);
198483
+ const sliceB = slice5(b);
198201
198484
  return [
198202
198485
  `Area: ${areaId}`,
198203
198486
  "",
198204
198487
  `--- doc A: ${a.path} ---`,
198205
- slice5(a),
198488
+ sliceA,
198206
198489
  `--- end doc A ---`,
198207
198490
  "",
198208
198491
  `--- doc B: ${b.path} ---`,
198209
- slice5(b),
198492
+ sliceB,
198210
198493
  `--- end doc B ---`,
198211
198494
  "",
198495
+ "SECTION OPTIONS \u2014 each side pointer MUST be one of these (verbatim), or the lead (heading: null):",
198496
+ "",
198497
+ `doc A (${a.path}):`,
198498
+ ...sectionOptions(sliceA),
198499
+ "",
198500
+ `doc B (${b.path}):`,
198501
+ ...sectionOptions(sliceB),
198502
+ "",
198212
198503
  "Return the JSON object as specified."
198213
198504
  ].join("\n");
198214
198505
  }
198215
198506
  var LlmOverlapSchema = external_exports.object({
198216
198507
  overlap: external_exports.boolean(),
198217
198508
  note: external_exports.string().default(""),
198218
- sections: external_exports.array(external_exports.object({ side: external_exports.enum(["A", "B"]), heading: external_exports.string().nullable() })).default([])
198509
+ sections: external_exports.array(external_exports.object({ side: external_exports.enum(["A", "B"]), heading: external_exports.string().nullable(), quote: external_exports.string().optional() })).default([])
198219
198510
  });
198220
198511
  var OverlapVerdictSchema = external_exports.object({
198221
198512
  overlap: external_exports.boolean(),
198222
198513
  note: external_exports.string().default(""),
198223
- sections: external_exports.array(external_exports.object({ doc: external_exports.string(), heading: external_exports.string().nullable() })).default([])
198514
+ sections: external_exports.array(external_exports.object({ doc: external_exports.string(), heading: external_exports.string().nullable(), quote: external_exports.string().optional() })).default([])
198224
198515
  });
198225
198516
  function spawnOverlapRunner(opts = {}) {
198226
198517
  const transport = opts.transport ?? cliTransport({ bin: opts.bin });
@@ -198240,7 +198531,11 @@ function spawnOverlapRunner(opts = {}) {
198240
198531
  return {
198241
198532
  overlap: inner.overlap,
198242
198533
  note: inner.note,
198243
- sections: inner.sections.map((s) => ({ doc: s.side === "A" ? a.path : b.path, heading: s.heading }))
198534
+ sections: inner.sections.map((s) => ({
198535
+ doc: s.side === "A" ? a.path : b.path,
198536
+ heading: s.heading,
198537
+ ...s.quote !== void 0 ? { quote: s.quote } : {}
198538
+ }))
198244
198539
  };
198245
198540
  };
198246
198541
  }
@@ -212438,7 +212733,7 @@ async function runHooksRun() {
212438
212733
 
212439
212734
  // tools/cli/src/index.ts
212440
212735
  var program2 = new Command();
212441
- program2.name("truecourse").version("0.7.0-next.6").description("TrueCourse CLI \u2014 analyze your repository and open the dashboard");
212736
+ program2.name("truecourse").version("0.7.0-next.8").description("TrueCourse CLI \u2014 analyze your repository and open the dashboard");
212442
212737
  function warnDiscontinued(command) {
212443
212738
  process.stderr.write(
212444
212739
  `\u26A0 \`${command}\` is discontinued in favor of \`truecourse guard\` \u2014 see the README. Planned removal: 0.8.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "truecourse",
3
- "version": "0.7.0-next.6",
3
+ "version": "0.7.0-next.8",
4
4
  "description": "Visualize your codebase architecture as an interactive graph",
5
5
  "type": "module",
6
6
  "bin": {