Files
ECC/scripts/lib/install/link-rewrite.js
T
754b8dd76c fix: make the installer runtime pass strict supply-chain vetting (#2503)
* fix: make the installer runtime pass strict supply-chain vetting

Remediate the four enterprise supply-chain vetting blockers from
affaan-m/ECC#2502 so the installer runtime (package.json + manifests +
scripts/lib/**) passes strict exact-pin evidence policy:

1. Remove the package.json `postinstall` lifecycle script (it only echoed a
   post-install banner) and move that banner to an explicit opt-in
   `npm run welcome` command. No install-time lifecycle script remains.
2. Exact-pin every dependency in package.json (dependencies + devDependencies)
   to the versions already resolved in package-lock.json; no ^/~ ranges.
3. Replace non-ASCII characters on the installer runtime script/config surface:
   em-dashes (U+2014) in scripts/lib/{path-safety,install-executor,
   install/link-rewrite}.js comments and the two "Itô" (U+00F4) occurrences in
   manifests/{install-components,install-modules}.json descriptions become
   ASCII, so strict-surface Unicode scanners are clean.
4. Drop the bare `require("ajv")` from scripts/lib/install-state.js; the file
   already carries a complete hand-rolled validator enforcing the same
   schemas/install-state.schema.json (ecc.install.v1) constraints, so the
   installer closure is dependency-free (zero non-builtin bare requires).

Refs affaan-m/ECC#2502

* fix: avoid unpinned welcome invocations

Signed-off-by: Samar Tomar <samar_tomar@hotmail.com>

* fix: validate translated skill frontmatter

Signed-off-by: Samar Tomar <samar_tomar@hotmail.com>

* fix: repair skill frontmatter YAML

Signed-off-by: Samar Tomar <samar_tomar@hotmail.com>

* fix: add MIT license to core skill manifests; pin verification-loop tsc invocation

* fix: preserve tsc/pyright exit status in verification-loop type-check (set -o pipefail)

* chore(deps): sync lockfiles with exact-pinned package.json

Regenerate package-lock.json and yarn.lock so the pinned dependency
specs are reflected in both lockfiles. npm ci and Yarn's --immutable
install now pass the sync check. The resolution tree is unchanged
(231 yarn resolutions, byte-identical set; zero npm transitive drift);
only the root descriptor strings move from ranges to the versions
already resolved in the committed lockfiles.

Addresses the Codex P1 on #2503.

---------

Signed-off-by: Samar Tomar <samar_tomar@hotmail.com>
Co-authored-by: Samarjeet Singh Tomar <samartomar@gmail.com>
2026-07-17 17:13:49 -04:00

180 lines
6.6 KiB
JavaScript

'use strict';
const path = require('path');
const posix = path.posix;
// Matches inline markdown links and images: `](target)` / `](target "title")`.
// We deliberately scope to the inline form because that is what skill/rule docs
// use for cross-directory references. Reference-style and autolinks are left
// untouched (they are rare in these files and carry higher false-positive risk).
const INLINE_LINK_PATTERN = /(!?\]\()([^()\s]+)(\s+"[^"]*")?(\))/g;
function toPosix(relativePath) {
return String(relativePath || '').replace(/\\/g, '/').replace(/^\.\//, '');
}
function stripTrailingSlash(value) {
return value.length > 1 ? value.replace(/\/+$/, '') : value;
}
// Build file + directory lookup maps from the plan's own file placements.
// `fileMappings` is a list of { sourceRel, destRel } where both are paths
// relative to the repo root and the install root respectively. The directory
// map is derived by walking shared ancestors of each source/dest pair, which is
// exact for prefix-insertion namespacing (e.g. `skills/x` -> `skills/ecc/x`):
// the path suffix below the inserted segment is preserved, so ancestor `k`
// of the source maps to the dest with the matching number of trailing
// segments removed.
function buildInstallIndex(fileMappings) {
const byFile = new Map();
const byDir = new Map();
for (const mapping of fileMappings || []) {
const sourceRel = toPosix(mapping.sourceRel);
const destRel = toPosix(mapping.destRel);
if (!sourceRel || !destRel) {
continue;
}
byFile.set(sourceRel, destRel);
const sourceParts = sourceRel.split('/');
const destParts = destRel.split('/');
// Map every source ancestor directory to its installed counterpart by
// removing the same count of trailing segments from the dest path.
for (let depth = 1; depth < sourceParts.length; depth += 1) {
const trailing = sourceParts.length - depth;
const destDepth = destParts.length - trailing;
if (destDepth < 1) {
continue;
}
const sourceDir = sourceParts.slice(0, depth).join('/');
const destDir = destParts.slice(0, destDepth).join('/');
// Only record real prefix-insertion mappings (suffix preserved). If a
// directory resolves to itself (no namespace change) we skip it so the
// rewriter leaves those links alone.
if (sourceDir !== destDir) {
byDir.set(sourceDir, destDir);
}
}
}
return { byFile, byDir };
}
function isExternalOrAnchor(target) {
return (
target === ''
|| target.startsWith('#')
|| target.startsWith('/')
|| target.startsWith('mailto:')
|| /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(target) // has a URL scheme (http:, https:, file:, ...)
);
}
// Resolve `target` (a relative link from `sourceDir`) to its repo-relative
// path, then return the install-relative path it should point to, or null when
// the target is not installed by this plan (leave such links untouched).
function resolveInstalledTarget(target, sourceDir, index) {
const hadTrailingSlash = target.endsWith('/');
const resolved = stripTrailingSlash(toPosix(posix.normalize(posix.join(sourceDir, target))));
// Escapes the repo root (starts with `..`) -> not something we placed.
if (resolved === '' || resolved === '.' || resolved.startsWith('..')) {
return null;
}
if (!hadTrailingSlash && index.byFile.has(resolved)) {
return { installed: index.byFile.get(resolved), trailingSlash: false };
}
if (index.byDir.has(resolved)) {
return { installed: index.byDir.get(resolved), trailingSlash: hadTrailingSlash };
}
return null;
}
// True when the plan installs `sourceRel` at a different relative path than the
// source (i.e. a namespace segment was injected, e.g. skills/x -> skills/ecc/x).
// Callers use this to keep non-namespaced files on the byte-for-byte copy path.
function isNamespacedSource(sourceRel, index) {
const normalizedSource = toPosix(sourceRel);
const installedSource = index && index.byFile.get(normalizedSource);
return Boolean(installedSource) && installedSource !== normalizedSource;
}
// Rewrite relative links in a single namespaced markdown file so they resolve
// to the file's installed location. Returns the content unchanged when the
// file itself was not namespaced or when no link needs adjustment. Pure: no IO.
function rewriteRelativeLinks(content, options) {
const { sourceRel, index } = options || {};
const normalizedSource = toPosix(sourceRel);
const installedSource = index && index.byFile.get(normalizedSource);
// Only rewrite when the file's own install path gained/changed a namespace
// segment. If it lands at the same relative path, every link recomputes to
// itself, so there is nothing to do.
if (!installedSource || installedSource === normalizedSource) {
return content;
}
const installedSourceDir = posix.dirname(installedSource);
const sourceDir = posix.dirname(normalizedSource);
const lines = String(content).split('\n');
let inFence = false;
for (let i = 0; i < lines.length; i += 1) {
const fenceToggle = /^\s*(```|~~~)/.test(lines[i]);
if (fenceToggle) {
inFence = !inFence;
continue;
}
if (inFence) {
continue; // never rewrite inside fenced code blocks
}
lines[i] = lines[i].replace(
INLINE_LINK_PATTERN,
(match, open, target, title, close) => {
// Preserve any `#fragment` so anchors survive the rewrite.
const hashIdx = target.indexOf('#');
const pathPart = hashIdx === -1 ? target : target.slice(0, hashIdx);
const fragment = hashIdx === -1 ? '' : target.slice(hashIdx);
if (isExternalOrAnchor(pathPart)) {
return match;
}
const resolution = resolveInstalledTarget(pathPart, sourceDir, index);
if (!resolution) {
return match;
}
let rewritten = posix.relative(installedSourceDir, resolution.installed);
if (rewritten === '') {
rewritten = '.';
}
if (resolution.trailingSlash && !rewritten.endsWith('/')) {
rewritten += '/';
}
// If the recomputed link points to the same place as the original
// (e.g. an intra-namespace `./sibling.md` whose endpoints both shift by
// the same prefix), keep the original text verbatim - including any
// leading `./` - so the rewrite stays a strict no-op where it must.
if (posix.normalize(rewritten) === posix.normalize(pathPart)) {
return match;
}
return `${open}${rewritten}${fragment}${title || ''}${close}`;
}
);
}
return lines.join('\n');
}
module.exports = {
buildInstallIndex,
isNamespacedSource,
rewriteRelativeLinks,
};