instar 1.3.35 → 1.3.37
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 +1 -0
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +53 -3
- package/dist/commands/server.js.map +1 -1
- package/dist/config/ConfigDefaults.d.ts.map +1 -1
- package/dist/config/ConfigDefaults.js +15 -0
- package/dist/config/ConfigDefaults.js.map +1 -1
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +15 -0
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/featureRolloutScan.d.ts +23 -0
- package/dist/core/featureRolloutScan.d.ts.map +1 -1
- package/dist/core/featureRolloutScan.js +102 -0
- package/dist/core/featureRolloutScan.js.map +1 -1
- package/dist/core/types.d.ts +45 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/messaging/AgentTelegramLedger.d.ts +65 -0
- package/dist/messaging/AgentTelegramLedger.d.ts.map +1 -0
- package/dist/messaging/AgentTelegramLedger.js +61 -0
- package/dist/messaging/AgentTelegramLedger.js.map +1 -0
- package/dist/messaging/ProcessedIdStore.d.ts +48 -0
- package/dist/messaging/ProcessedIdStore.d.ts.map +1 -0
- package/dist/messaging/ProcessedIdStore.js +111 -0
- package/dist/messaging/ProcessedIdStore.js.map +1 -0
- package/dist/monitoring/ReleaseReadinessSentinel.d.ts +172 -0
- package/dist/monitoring/ReleaseReadinessSentinel.d.ts.map +1 -0
- package/dist/monitoring/ReleaseReadinessSentinel.js +315 -0
- package/dist/monitoring/ReleaseReadinessSentinel.js.map +1 -0
- package/dist/monitoring/releaseReadinessWiring.d.ts +54 -0
- package/dist/monitoring/releaseReadinessWiring.d.ts.map +1 -0
- package/dist/monitoring/releaseReadinessWiring.js +241 -0
- package/dist/monitoring/releaseReadinessWiring.js.map +1 -0
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +5 -0
- package/dist/scaffold/templates.js.map +1 -1
- package/dist/server/AgentServer.d.ts +1 -0
- package/dist/server/AgentServer.d.ts.map +1 -1
- package/dist/server/AgentServer.js +1 -0
- package/dist/server/AgentServer.js.map +1 -1
- package/dist/server/CapabilityIndex.d.ts.map +1 -1
- package/dist/server/CapabilityIndex.js +14 -0
- package/dist/server/CapabilityIndex.js.map +1 -1
- package/dist/server/routes.d.ts +3 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +55 -0
- package/dist/server/routes.js.map +1 -1
- package/package.json +1 -1
- package/scripts/analyze-release.js +221 -0
- package/scripts/upgrade-guide-validator.mjs +120 -0
- package/src/data/builtin-manifest.json +63 -63
- package/src/scaffold/templates/jobs/instar/release-readiness-check.md +24 -0
- package/src/scaffold/templates.ts +5 -0
- package/upgrades/1.3.37.md +59 -0
- package/upgrades/side-effects/a2a-ledger-and-processed-id-store.md +68 -0
- package/upgrades/side-effects/release-readiness-visibility-pr2a.md +27 -0
- package/upgrades/side-effects/release-readiness-visibility-pr2b.md +26 -0
- package/upgrades/side-effects/release-readiness-visibility-pr2c.md +37 -0
- package/upgrades/side-effects/release-readiness-visibility-pr3.md +33 -0
package/package.json
CHANGED
|
@@ -36,6 +36,11 @@ const ROOT = path.resolve(__dirname, '..');
|
|
|
36
36
|
const args = process.argv.slice(2);
|
|
37
37
|
const JSON_ONLY = args.includes('--json');
|
|
38
38
|
const RECOMMEND_ONLY = args.includes('--recommend-only');
|
|
39
|
+
// --draft-guide writes/merges upgrades/NEXT.md from the computed change-list
|
|
40
|
+
// (Layer A of the release-readiness-visibility spec). Drafted content carries
|
|
41
|
+
// `auto-draft-unreviewed` markers that BOTH publish gates reject until a human
|
|
42
|
+
// reviews each section — so auto-fill can never ship un-reviewed notes.
|
|
43
|
+
const DRAFT_GUIDE = args.includes('--draft-guide');
|
|
39
44
|
|
|
40
45
|
// --ref=<rev> selects the tip the analysis runs against (default HEAD).
|
|
41
46
|
// Layer B of the release-readiness-visibility spec passes --ref=FETCH_HEAD so the
|
|
@@ -584,6 +589,214 @@ function generateChangeDescriptions(analysis) {
|
|
|
584
589
|
return descriptions;
|
|
585
590
|
}
|
|
586
591
|
|
|
592
|
+
// ── Auto-draft (Layer A of release-readiness-visibility spec) ────────
|
|
593
|
+
//
|
|
594
|
+
// Turns the computed change-list into a NEXT.md draft. Every drafted item
|
|
595
|
+
// carries an `auto-draft-unreviewed` marker. The publish gates
|
|
596
|
+
// (check-upgrade-guide.js validator + publish.yml skip predicate) refuse to
|
|
597
|
+
// ship a guide that still has those markers, so auto-fill removes the "blank
|
|
598
|
+
// guide" root cause WITHOUT defeating the human-review purpose of the gate.
|
|
599
|
+
|
|
600
|
+
const UNREVIEWED_BLOCK_MARKER = '<!-- auto-draft-unreviewed-block -->';
|
|
601
|
+
const UNCOVERED_BEGIN = '<!-- BEGIN auto-draft-uncovered (unreviewed) -->';
|
|
602
|
+
const UNCOVERED_END = '<!-- END auto-draft-uncovered (unreviewed) -->';
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Neutralize commit-message-sourced text before it lands in a published guide:
|
|
606
|
+
* - strip HTML comments (prevents forging `<!-- bump: -->` / unreviewed markers)
|
|
607
|
+
* - collapse whitespace, cap length
|
|
608
|
+
* - escape leading markdown control chars so an item can't break section bounds
|
|
609
|
+
*/
|
|
610
|
+
function sanitizeDraftText(s) {
|
|
611
|
+
let t = String(s ?? '')
|
|
612
|
+
.replace(/<!--[\s\S]*?-->/g, '') // strip HTML comments
|
|
613
|
+
.replace(/\s+/g, ' ')
|
|
614
|
+
.trim();
|
|
615
|
+
if (t.length > 200) t = t.slice(0, 197).trimEnd() + '…';
|
|
616
|
+
// Escape leading control sequences (heading / hr) so a crafted message can't
|
|
617
|
+
// inject a new section boundary.
|
|
618
|
+
t = t.replace(/^(#{1,6}\s)/, '\\$1').replace(/^(-{3,})/, '\\$1');
|
|
619
|
+
return t;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
const FALLBACK_USER_IMPACTS = new Set([
|
|
623
|
+
'Review the commit for user-facing changes',
|
|
624
|
+
'Review the commit for specifics',
|
|
625
|
+
]);
|
|
626
|
+
|
|
627
|
+
function unreviewed(slug) {
|
|
628
|
+
return `<!-- auto-draft-unreviewed: ${slug} -->`;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/** Does the existing guide already mention this change description? */
|
|
632
|
+
function descriptionMentioned(desc, content) {
|
|
633
|
+
const lc = content.toLowerCase();
|
|
634
|
+
// Endpoint: the path token (e.g. "/release-readiness").
|
|
635
|
+
const ep = /:\s*[A-Z]+\s+(\/\S+)/.exec(desc.summary);
|
|
636
|
+
if (ep) return lc.includes(ep[1].toLowerCase());
|
|
637
|
+
// CLI command: "instar <cmd>".
|
|
638
|
+
const cmd = /instar\s+([a-z][\w:-]*)/i.exec(desc.summary);
|
|
639
|
+
if (cmd) return lc.includes(cmd[1].toLowerCase());
|
|
640
|
+
// Config field: "New config option: <field>".
|
|
641
|
+
const cfg = /config option:\s*(\w+)/i.exec(desc.summary);
|
|
642
|
+
if (cfg) return lc.includes(cfg[1].toLowerCase());
|
|
643
|
+
// Otherwise (commit-derived): any >4-char keyword from the summary.
|
|
644
|
+
const kws = desc.summary.replace(/^(feat|fix)[:(]?\s*/i, '').split(/\s+/).filter((w) => w.length > 4).slice(0, 4);
|
|
645
|
+
return kws.length > 0 && kws.some((k) => lc.includes(k.toLowerCase()));
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/** One "## What Changed" bullet for a description. */
|
|
649
|
+
function changedBullet(desc) {
|
|
650
|
+
return `- **${desc.type}**: ${sanitizeDraftText(desc.summary)} — ${sanitizeDraftText(desc.detail)}`;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/** Build the three required sections' bodies from the change-list. */
|
|
654
|
+
function buildSectionBodies(changeDescriptions, hasFix) {
|
|
655
|
+
const whatChanged = changeDescriptions.length
|
|
656
|
+
? changeDescriptions.map(changedBullet).join('\n')
|
|
657
|
+
: '- (no structurally-detected changes; describe the release manually)';
|
|
658
|
+
|
|
659
|
+
// "What to Tell Your User" must stay plain — no backticks/camelCase/code.
|
|
660
|
+
// Use the generic userImpact lines (already plain) and a HUMAN-REQUIRED note
|
|
661
|
+
// for any commit whose impact the analyzer couldn't infer.
|
|
662
|
+
const userLines = [];
|
|
663
|
+
const seen = new Set();
|
|
664
|
+
for (const d of changeDescriptions) {
|
|
665
|
+
const impact = FALLBACK_USER_IMPACTS.has(d.userImpact)
|
|
666
|
+
? 'HUMAN-REQUIRED: describe what this means for the user'
|
|
667
|
+
: d.userImpact;
|
|
668
|
+
const line = `- ${sanitizeDraftText(impact)}`;
|
|
669
|
+
if (!seen.has(line)) { seen.add(line); userLines.push(line); }
|
|
670
|
+
}
|
|
671
|
+
const tellUser = userLines.length ? userLines.join('\n') : '- HUMAN-REQUIRED: summarize the user-facing impact';
|
|
672
|
+
|
|
673
|
+
const capRows = changeDescriptions
|
|
674
|
+
.filter((d) => d.type === 'feature' || d.type === 'enhancement')
|
|
675
|
+
.map((d) => `| ${sanitizeDraftText(d.summary)} | ${sanitizeDraftText(d.agentImpact)} |`)
|
|
676
|
+
.join('\n') || '| (none) | — |';
|
|
677
|
+
|
|
678
|
+
let out =
|
|
679
|
+
`## What Changed
|
|
680
|
+
|
|
681
|
+
${unreviewed('what-changed')}
|
|
682
|
+
${whatChanged}
|
|
683
|
+
|
|
684
|
+
## What to Tell Your User
|
|
685
|
+
|
|
686
|
+
${unreviewed('tell-user')}
|
|
687
|
+
${tellUser}
|
|
688
|
+
|
|
689
|
+
## Summary of New Capabilities
|
|
690
|
+
|
|
691
|
+
${unreviewed('capabilities')}
|
|
692
|
+
| Capability | How to Use |
|
|
693
|
+
|-----------|-----------|
|
|
694
|
+
${capRows}
|
|
695
|
+
`;
|
|
696
|
+
|
|
697
|
+
if (hasFix) {
|
|
698
|
+
out +=
|
|
699
|
+
`
|
|
700
|
+
## Evidence
|
|
701
|
+
|
|
702
|
+
${unreviewed('evidence')}
|
|
703
|
+
HUMAN-REQUIRED: reproduction + observed before/after, or "Not reproducible in dev — [concrete reason]". Unit tests passing is not evidence.
|
|
704
|
+
`;
|
|
705
|
+
}
|
|
706
|
+
return out;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/** Full NEXT.md draft (used when the guide is absent or a pristine template). */
|
|
710
|
+
function buildFullDraft(changeDescriptions, recommended, hasFix) {
|
|
711
|
+
return `# Upgrade Guide — vNEXT
|
|
712
|
+
|
|
713
|
+
<!-- bump: ${recommended} -->
|
|
714
|
+
${UNREVIEWED_BLOCK_MARKER}
|
|
715
|
+
<!-- Auto-drafted from the classified commit range. Every section below carries an
|
|
716
|
+
auto-draft-unreviewed marker. A human reviews each section and replaces its
|
|
717
|
+
marker with a reviewed-by receipt before this guide can publish.
|
|
718
|
+
See docs/specs/RELEASE-READINESS-VISIBILITY-SPEC.md §4.1.1. -->
|
|
719
|
+
|
|
720
|
+
${buildSectionBodies(changeDescriptions, hasFix)}`;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/** The uncovered-delta block appended to a guide that already has human content. */
|
|
724
|
+
function buildUncoveredBlock(changeDescriptions, existingContent) {
|
|
725
|
+
const uncovered = changeDescriptions.filter((d) => !descriptionMentioned(d, existingContent));
|
|
726
|
+
if (uncovered.length === 0) return '';
|
|
727
|
+
const items = uncovered.map((d) => `- ${unreviewed(slugifyDesc(d))} **${d.type}**: ${sanitizeDraftText(d.summary)}`).join('\n');
|
|
728
|
+
return `${UNCOVERED_BEGIN}
|
|
729
|
+
<!-- These changes were detected in the commit range but not yet mentioned above.
|
|
730
|
+
Fold them into the sections above (or describe why they need no note), then
|
|
731
|
+
remove each marker with a reviewed-by receipt. -->
|
|
732
|
+
${items}
|
|
733
|
+
${UNCOVERED_END}`;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function slugifyDesc(d) {
|
|
737
|
+
return (d.summary || 'item').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 48) || 'item';
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
const PRISTINE_TEMPLATE = (c) => c.includes('[Feature name]') && c.includes('[Capability]');
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* Write or merge upgrades/NEXT.md. Race-guarded against publish finalize.
|
|
744
|
+
* Returns a short status string for logging.
|
|
745
|
+
*/
|
|
746
|
+
function writeOrMergeGuide(changeDescriptions, recommended, hasFix) {
|
|
747
|
+
const upgradesDir = path.join(ROOT, 'upgrades');
|
|
748
|
+
const nextPath = path.join(upgradesDir, 'NEXT.md');
|
|
749
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf-8'));
|
|
750
|
+
const versionGuide = path.join(upgradesDir, `${pkg.version}.md`);
|
|
751
|
+
|
|
752
|
+
fs.mkdirSync(upgradesDir, { recursive: true });
|
|
753
|
+
|
|
754
|
+
// Race guard against publish-finalize: refuse to draft once a finalize has
|
|
755
|
+
// produced upgrades/{version}.md. Per RELEASE-READINESS-VISIBILITY-SPEC §4.1.3
|
|
756
|
+
// this version-file check IS the cross-host guarantee (finalize renames
|
|
757
|
+
// NEXT.md → {version}.md in CI on a separate checkout; git's ref-update
|
|
758
|
+
// atomicity covers the merge). The intra-host O_EXCL advisory lock the spec
|
|
759
|
+
// also describes is intentionally omitted here: releasing it requires a
|
|
760
|
+
// destructive fs.unlink (forbidden by lint-no-direct-destructive in this
|
|
761
|
+
// dependency-free, test-copyable script), and the draft job is single-runner
|
|
762
|
+
// per the multi-machine lease, so concurrent local drafters don't occur.
|
|
763
|
+
if (fs.existsSync(versionGuide)) {
|
|
764
|
+
return `skipped: upgrades/${pkg.version}.md already finalized — not drafting`;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
const existing = fs.existsSync(nextPath) ? fs.readFileSync(nextPath, 'utf-8') : '';
|
|
768
|
+
|
|
769
|
+
if (!existing.trim() || PRISTINE_TEMPLATE(existing)) {
|
|
770
|
+
fs.writeFileSync(nextPath, buildFullDraft(changeDescriptions, recommended, hasFix));
|
|
771
|
+
return 'wrote full draft (guide was absent or a pristine template)';
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// Human content present — additive, never-clobber. Regenerate only the
|
|
775
|
+
// uncovered-delta block (idempotent). Coverage is measured against the
|
|
776
|
+
// HUMAN content only: strip any prior auto-block first, otherwise the
|
|
777
|
+
// block's own text would count as "coverage" and the block would
|
|
778
|
+
// oscillate (present on one run, absent the next).
|
|
779
|
+
const beginIdx = existing.indexOf(UNCOVERED_BEGIN);
|
|
780
|
+
const endIdx = existing.indexOf(UNCOVERED_END);
|
|
781
|
+
let tail = '';
|
|
782
|
+
let humanContent = existing;
|
|
783
|
+
if (beginIdx !== -1) {
|
|
784
|
+
tail = endIdx !== -1 ? existing.slice(endIdx + UNCOVERED_END.length) : '';
|
|
785
|
+
humanContent = existing.slice(0, beginIdx) + tail;
|
|
786
|
+
}
|
|
787
|
+
const block = buildUncoveredBlock(changeDescriptions, humanContent);
|
|
788
|
+
let body;
|
|
789
|
+
if (beginIdx !== -1) {
|
|
790
|
+
body = existing.slice(0, beginIdx).replace(/\s+$/, '') + (block ? `\n\n${block}` : '') + tail;
|
|
791
|
+
} else if (block) {
|
|
792
|
+
body = existing.replace(/\s+$/, '') + `\n\n${block}\n`;
|
|
793
|
+
} else {
|
|
794
|
+
return 'no uncovered changes — guide already covers the change-list';
|
|
795
|
+
}
|
|
796
|
+
fs.writeFileSync(nextPath, body);
|
|
797
|
+
return block ? 'merged uncovered-delta block into existing guide' : 'removed stale uncovered-delta block (now fully covered)';
|
|
798
|
+
}
|
|
799
|
+
|
|
587
800
|
// ── Main ─────────────────────────────────────────────────────────────
|
|
588
801
|
|
|
589
802
|
const lastTag = getLastReleaseTag();
|
|
@@ -645,6 +858,14 @@ if (bumpRecommendation.patchSignals.length > 0) {
|
|
|
645
858
|
for (const s of bumpRecommendation.patchSignals) log(` - ${s}`);
|
|
646
859
|
}
|
|
647
860
|
|
|
861
|
+
if (DRAFT_GUIDE) {
|
|
862
|
+
const hasFix = analysis.commits.fixes.length > 0;
|
|
863
|
+
const status = writeOrMergeGuide(changeDescriptions, bumpRecommendation.recommended, hasFix);
|
|
864
|
+
log(`\n Draft guide: ${status}`);
|
|
865
|
+
console.log(status);
|
|
866
|
+
process.exit(0);
|
|
867
|
+
}
|
|
868
|
+
|
|
648
869
|
if (RECOMMEND_ONLY) {
|
|
649
870
|
console.log(bumpRecommendation.recommended);
|
|
650
871
|
process.exit(0);
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
// The CLI entry point (check-upgrade-guide.js) wraps this with I/O.
|
|
3
3
|
// Extracted for unit testability.
|
|
4
4
|
|
|
5
|
+
import crypto from 'node:crypto';
|
|
6
|
+
|
|
5
7
|
export const REQUIRED_SECTIONS = [
|
|
6
8
|
'## What Changed',
|
|
7
9
|
'## What to Tell Your User',
|
|
@@ -207,6 +209,9 @@ export function validateGuideContent(content) {
|
|
|
207
209
|
issues.push(...evidenceIssues(content));
|
|
208
210
|
}
|
|
209
211
|
|
|
212
|
+
// Auto-draft review gate — unreviewed markers + receipt validity.
|
|
213
|
+
issues.push(...autoDraftReviewIssues(content));
|
|
214
|
+
|
|
210
215
|
return issues;
|
|
211
216
|
}
|
|
212
217
|
|
|
@@ -218,3 +223,118 @@ export function parseBumpType(content) {
|
|
|
218
223
|
const match = /<!--\s*bump:\s*(patch|minor|major)\s*-->/.exec(content);
|
|
219
224
|
return match ? match[1] : null;
|
|
220
225
|
}
|
|
226
|
+
|
|
227
|
+
// ── Auto-draft review gate (release-readiness-visibility spec §4.1.1) ──
|
|
228
|
+
//
|
|
229
|
+
// Layer A auto-drafts NEXT.md from the classified commit range. Every drafted
|
|
230
|
+
// section carries an `auto-draft-unreviewed` marker. The publish gate refuses
|
|
231
|
+
// to ship a guide while any such marker remains, so auto-fill removes the
|
|
232
|
+
// "blank guide" root cause WITHOUT shipping un-reviewed notes. A human signals
|
|
233
|
+
// review by REPLACING a section's marker with a hash-locked `reviewed-by`
|
|
234
|
+
// receipt; editing the section afterward invalidates the hash and re-blocks.
|
|
235
|
+
|
|
236
|
+
export const REVIEW_RECEIPT_MAX_AGE_DAYS = 30;
|
|
237
|
+
|
|
238
|
+
const UNREVIEWED_MARKER_RE = /<!--\s*auto-draft-unreviewed(?:-block)?(?::[^>]*?)?\s*-->/g;
|
|
239
|
+
const REVIEW_RECEIPT_RE = /<!--\s*reviewed-by:\s*(.+?)\s*@\s*([0-9T:.+\-Z]+)\s*:hash=([a-f0-9]{64})\s*-->/gi;
|
|
240
|
+
// A `reviewed-by` comment that does NOT match the strict form above — used to
|
|
241
|
+
// catch receipts missing the required :hash= (the iter-3 V2 fix).
|
|
242
|
+
const REVIEW_RECEIPT_LOOSE_RE = /<!--\s*reviewed-by:[^>]*-->/gi;
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Canonicalize a section body for hashing: LF line endings, drop the receipt
|
|
246
|
+
* line itself, strip trailing whitespace per line, trim. Mirrors the rule in
|
|
247
|
+
* RELEASE-READINESS-VISIBILITY-SPEC §4.1.1.
|
|
248
|
+
*/
|
|
249
|
+
export function canonicalizeSectionForHash(body) {
|
|
250
|
+
return String(body)
|
|
251
|
+
.replace(/\r\n/g, '\n')
|
|
252
|
+
.split('\n')
|
|
253
|
+
.filter((l) => !/<!--\s*reviewed-by:/i.test(l))
|
|
254
|
+
.map((l) => l.replace(/\s+$/, ''))
|
|
255
|
+
.join('\n')
|
|
256
|
+
.trim();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function sectionReviewHash(body) {
|
|
260
|
+
return crypto.createHash('sha256').update(canonicalizeSectionForHash(body)).digest('hex');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Split content into H2 sections: [{ title, body }]. */
|
|
264
|
+
function h2Sections(content) {
|
|
265
|
+
const out = [];
|
|
266
|
+
const re = /^## (.+)$/gm;
|
|
267
|
+
let m;
|
|
268
|
+
const heads = [];
|
|
269
|
+
while ((m = re.exec(content)) !== null) heads.push({ title: m[1].trim(), idx: m.index, after: re.lastIndex });
|
|
270
|
+
for (let i = 0; i < heads.length; i++) {
|
|
271
|
+
const end = i + 1 < heads.length ? heads[i + 1].idx : content.length;
|
|
272
|
+
out.push({ title: heads[i].title, body: content.slice(heads[i].after, end) });
|
|
273
|
+
}
|
|
274
|
+
return out;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Gate issues for the auto-draft review state. Returns issue strings (empty = ok):
|
|
279
|
+
* 1. Any remaining `auto-draft-unreviewed` marker blocks publish.
|
|
280
|
+
* 2. A `reviewed-by` receipt missing the required `:hash=` blocks.
|
|
281
|
+
* 3. A receipt whose hash doesn't match its (canonicalized) section blocks
|
|
282
|
+
* — i.e. the section was edited after review.
|
|
283
|
+
* 4. A receipt older than REVIEW_RECEIPT_MAX_AGE_DAYS blocks.
|
|
284
|
+
* (Full "marker stripped without a receipt" detection needs a git-diff and is
|
|
285
|
+
* the tracked Phase-2 CI check per §4.1.1 — out of scope for this snapshot gate.)
|
|
286
|
+
*/
|
|
287
|
+
export function autoDraftReviewIssues(content, now = Date.now()) {
|
|
288
|
+
const issues = [];
|
|
289
|
+
|
|
290
|
+
const unreviewed = content.match(UNREVIEWED_MARKER_RE) || [];
|
|
291
|
+
if (unreviewed.length > 0) {
|
|
292
|
+
issues.push(
|
|
293
|
+
`guide has ${unreviewed.length} unreviewed auto-draft marker(s) — a human must review each section and ` +
|
|
294
|
+
`replace its 'auto-draft-unreviewed' marker with a 'reviewed-by: <name> @ <ISO-date> :hash=<sha256>' receipt ` +
|
|
295
|
+
`before this guide can publish (RELEASE-READINESS-VISIBILITY-SPEC §4.1.1)`,
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Loose receipts that don't match the strict (with :hash=) form.
|
|
300
|
+
const looseAll = content.match(REVIEW_RECEIPT_LOOSE_RE) || [];
|
|
301
|
+
for (const loose of looseAll) {
|
|
302
|
+
REVIEW_RECEIPT_RE.lastIndex = 0;
|
|
303
|
+
if (!REVIEW_RECEIPT_RE.test(loose)) {
|
|
304
|
+
issues.push(
|
|
305
|
+
`malformed reviewed-by receipt (missing required ':hash=<sha256>' or bad date): ${loose.trim()}. ` +
|
|
306
|
+
`Required form: <!-- reviewed-by: <name> @ <ISO-date> :hash=<sha256> -->`,
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Strict receipts: validate hash-match against their section + age window.
|
|
312
|
+
for (const section of h2Sections(content)) {
|
|
313
|
+
REVIEW_RECEIPT_RE.lastIndex = 0;
|
|
314
|
+
let r;
|
|
315
|
+
while ((r = REVIEW_RECEIPT_RE.exec(section.body)) !== null) {
|
|
316
|
+
const [, , dateStr, hash] = r;
|
|
317
|
+
const ts = Date.parse(dateStr);
|
|
318
|
+
if (Number.isNaN(ts)) {
|
|
319
|
+
issues.push(`reviewed-by receipt in "## ${section.title}" has an unparseable date: "${dateStr}"`);
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
const ageDays = (now - ts) / (24 * 60 * 60 * 1000);
|
|
323
|
+
if (ageDays > REVIEW_RECEIPT_MAX_AGE_DAYS) {
|
|
324
|
+
issues.push(
|
|
325
|
+
`reviewed-by receipt in "## ${section.title}" is ${Math.floor(ageDays)} days old ` +
|
|
326
|
+
`(max ${REVIEW_RECEIPT_MAX_AGE_DAYS}) — re-review the section and refresh the receipt`,
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
const actual = sectionReviewHash(section.body);
|
|
330
|
+
if (actual !== hash) {
|
|
331
|
+
issues.push(
|
|
332
|
+
`reviewed-by receipt in "## ${section.title}" hash mismatch — the section was edited after review. ` +
|
|
333
|
+
`Re-review and update :hash= to ${actual}`,
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return issues;
|
|
340
|
+
}
|