dorfl 0.14.1 → 0.14.3

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.
Files changed (66) hide show
  1. package/dist/advance-ci-template.d.ts.map +1 -1
  2. package/dist/advance-ci-template.js +10 -4
  3. package/dist/advance-ci-template.js.map +1 -1
  4. package/dist/advance-lifecycle-template.d.ts.map +1 -1
  5. package/dist/advance-lifecycle-template.js +78 -25
  6. package/dist/advance-lifecycle-template.js.map +1 -1
  7. package/dist/cli.js +2 -2
  8. package/dist/close-job-template.d.ts +1 -1
  9. package/dist/close-job-template.d.ts.map +1 -1
  10. package/dist/close-job-template.js +17 -3
  11. package/dist/close-job-template.js.map +1 -1
  12. package/dist/install-ci-action-pins.d.ts +98 -0
  13. package/dist/install-ci-action-pins.d.ts.map +1 -0
  14. package/dist/install-ci-action-pins.js +165 -0
  15. package/dist/install-ci-action-pins.js.map +1 -0
  16. package/dist/install-ci-core.d.ts +84 -0
  17. package/dist/install-ci-core.d.ts.map +1 -1
  18. package/dist/install-ci-core.js +120 -14
  19. package/dist/install-ci-core.js.map +1 -1
  20. package/dist/install-ci-github.d.ts +15 -1
  21. package/dist/install-ci-github.d.ts.map +1 -1
  22. package/dist/install-ci-github.js +32 -0
  23. package/dist/install-ci-github.js.map +1 -1
  24. package/dist/install-ci.d.ts.map +1 -1
  25. package/dist/install-ci.js +21 -0
  26. package/dist/install-ci.js.map +1 -1
  27. package/dist/intake-trigger-template.d.ts.map +1 -1
  28. package/dist/intake-trigger-template.js +24 -7
  29. package/dist/intake-trigger-template.js.map +1 -1
  30. package/dist/intake.d.ts.map +1 -1
  31. package/dist/intake.js +10 -5
  32. package/dist/intake.js.map +1 -1
  33. package/dist/mint-adr.d.ts.map +1 -1
  34. package/dist/mint-adr.js +4 -1
  35. package/dist/mint-adr.js.map +1 -1
  36. package/dist/slug-namespace.d.ts +4 -3
  37. package/dist/slug-namespace.d.ts.map +1 -1
  38. package/dist/slug-namespace.js +28 -3
  39. package/dist/slug-namespace.js.map +1 -1
  40. package/dist/slug-safety.d.ts +55 -0
  41. package/dist/slug-safety.d.ts.map +1 -0
  42. package/dist/slug-safety.js +76 -0
  43. package/dist/slug-safety.js.map +1 -0
  44. package/dist/triage-persist.d.ts.map +1 -1
  45. package/dist/triage-persist.js +12 -3
  46. package/dist/triage-persist.js.map +1 -1
  47. package/dist/verify-workflow-template.d.ts +1 -1
  48. package/dist/verify-workflow-template.d.ts.map +1 -1
  49. package/dist/verify-workflow-template.js +16 -3
  50. package/dist/verify-workflow-template.js.map +1 -1
  51. package/package.json +3 -2
  52. package/src/advance-ci-template.ts +12 -4
  53. package/src/advance-lifecycle-template.ts +80 -25
  54. package/src/cli.ts +2 -2
  55. package/src/close-job-template.ts +17 -3
  56. package/src/install-ci-action-pins.ts +184 -0
  57. package/src/install-ci-core.ts +166 -14
  58. package/src/install-ci-github.ts +40 -1
  59. package/src/install-ci.ts +25 -0
  60. package/src/intake-trigger-template.ts +28 -7
  61. package/src/intake.ts +10 -5
  62. package/src/mint-adr.ts +4 -1
  63. package/src/slug-namespace.ts +33 -5
  64. package/src/slug-safety.ts +83 -0
  65. package/src/triage-persist.ts +13 -7
  66. package/src/verify-workflow-template.ts +16 -3
@@ -0,0 +1,83 @@
1
+ /**
2
+ * The SAFE-SLUG character set, and the one sanitiser every slug-producing path
3
+ * shares.
4
+ *
5
+ * A slug is not just a label: it becomes a file name (`work/<folder>/<slug>.md`),
6
+ * part of a git ref (`work/<slug>`, `work/intake-task-<slug>`), a command-line
7
+ * argument (`dorfl advance "task:<slug>"`, one per CI matrix leg) and text in
8
+ * commit messages. Several producers draft slugs from content an outsider can
9
+ * influence: `intake` from an issue's title and thread, the triage and apply
10
+ * rungs from an agent verdict. Before this module the only transform was
11
+ * `paramCase`, which keeps `$(`, backticks, quotes, `;`, `|` and `/`, and the
12
+ * triage/ADR paths only `trim()`med, so a drafted `../x` escaped its folder.
13
+ *
14
+ * Two functions, two jobs:
15
+ *
16
+ * - {@link toSafeSlug} is for PRODUCERS: it turns any drafted text into a slug
17
+ * made only of ASCII letters, digits and single hyphens. Its output always
18
+ * passes {@link isSafeSlug}.
19
+ * - {@link isSafeSlug} is for CONSUMERS: the slug resolvers refuse any slug
20
+ * outside the safe set, which covers slugs no producer here minted (a
21
+ * frontmatter `slug:` a human or agent wrote by hand). It is deliberately a
22
+ * little wider than what {@link toSafeSlug} emits (it also allows `.` and
23
+ * `_` and mixed case) so every slug already in use in a real `work/` tree
24
+ * keeps resolving: those characters are inert in a file name, a ref and a
25
+ * quoted shell word. `..` is still refused (path traversal), as is a leading
26
+ * `-` (it would read as a command-line option) or `.` (a hidden file), and so
27
+ * is a `.lock` suffix (git refuses it in a ref component).
28
+ */
29
+
30
+ /**
31
+ * The longest slug either function accepts. Real slugs in this repo top out
32
+ * around 105 characters; 120 leaves room without letting a drafted essay become
33
+ * a file name or ref component.
34
+ */
35
+ export const MAX_SLUG_LENGTH = 120;
36
+
37
+ /** Letters, digits, `.`, `_`, `-`; starts and ends with a letter or digit. */
38
+ const SAFE_SLUG_RE = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
39
+
40
+ /** Git refuses a ref component ending in `.lock` (its lock-file suffix). */
41
+ const GIT_LOCK_SUFFIX_RE = /\.lock$/i;
42
+
43
+ /**
44
+ * Whether `slug` is safe to use as a file name, a git ref component and a
45
+ * command-line argument (see the module doc for the exact set).
46
+ */
47
+ export function isSafeSlug(slug: string): boolean {
48
+ return (
49
+ slug.length > 0 &&
50
+ slug.length <= MAX_SLUG_LENGTH &&
51
+ SAFE_SLUG_RE.test(slug) &&
52
+ !slug.includes('..') &&
53
+ !GIT_LOCK_SUFFIX_RE.test(slug)
54
+ );
55
+ }
56
+
57
+ /**
58
+ * Turn drafted text into a safe slug: accents are folded to their base letter,
59
+ * every run of anything other than an ASCII letter or digit becomes ONE hyphen,
60
+ * leading/trailing hyphens are dropped, and the result is capped at
61
+ * {@link MAX_SLUG_LENGTH}. Case is preserved (callers that want lower case, like
62
+ * `intake`, `paramCase` first). Returns `''` when nothing usable is left, so the
63
+ * caller's existing "no slug to derive" refusal fires instead of a junk name.
64
+ */
65
+ export function toSafeSlug(input: string): string {
66
+ return input
67
+ .normalize('NFKD')
68
+ .replace(/[\u0300-\u036f]/g, '')
69
+ .replace(/[^A-Za-z0-9]+/g, '-')
70
+ .replace(/^-+|-+$/g, '')
71
+ .slice(0, MAX_SLUG_LENGTH)
72
+ .replace(/-+$/, '');
73
+ }
74
+
75
+ /**
76
+ * Keep a slug that is already safe EXACTLY as it is, and sanitise one that is
77
+ * not. The producers use this so every input that worked before still yields
78
+ * the same slug, and only an unsafe one changes.
79
+ */
80
+ export function ensureSafeSlug(input: string): string {
81
+ const trimmed = input.trim();
82
+ return isSafeSlug(trimmed) ? trimmed : toSafeSlug(trimmed);
83
+ }
@@ -11,6 +11,7 @@ import {resolveSidecarIdentity, sidecarPathFor} from './sidecar.js';
11
11
  import type {TriageAutoKind} from './triage-gate.js';
12
12
  import {renderTaskBody, renderSpecBody} from './buildable-body.js';
13
13
  import {setFrontmatterMarker} from './frontmatter.js';
14
+ import {ensureSafeSlug} from './slug-safety.js';
14
15
 
15
16
  /**
16
17
  * The engine-owned TRIAGE PERSIST (spec `advance-loop`, task `advance-rung-triage`,
@@ -433,7 +434,9 @@ export async function promoteObservation(
433
434
  const note = options.note ?? (() => {});
434
435
 
435
436
  const {slug: obsSlug} = resolveSidecarIdentity(item);
436
- const newSlug = (options.newSlug ?? obsSlug).trim();
437
+ // The drafted slug comes from an agent verdict: an unsafe one (a `/`, `..`,
438
+ // shell metacharacters) is sanitised, a safe one is kept exactly as drafted.
439
+ const newSlug = ensureSafeSlug(options.newSlug ?? obsSlug);
437
440
  const artifact = options.artifact ?? 'task';
438
441
  if (newSlug === '') {
439
442
  return {
@@ -460,13 +463,16 @@ export async function promoteObservation(
460
463
  // review-nits back-pointer); this is the triage-promote lineage. Matching on this
461
464
  // explicit field — not the slug — is what keeps an unrelated same-slug task from
462
465
  // false-positiving into a benign skip.
466
+ // A drafted `stubContent` carries its own frontmatter, and the ledger reads
467
+ // `slug:` before the file name, so stamp the SAFE slug over whatever the
468
+ // draft said: otherwise a drafted `slug: ../x` would become the item's
469
+ // identity even though the file name was sanitised.
470
+ const drafted =
471
+ options.stubContent !== undefined
472
+ ? setFrontmatterMarker(options.stubContent, 'slug', newSlug)
473
+ : buildPromotedBody(artifact, newSlug, readItem(cwd, itemPath, env));
463
474
  const content = setFrontmatterMarker(
464
- ensureTaskDispatchable(
465
- artifact,
466
- newSlug,
467
- options.stubContent ??
468
- buildPromotedBody(artifact, newSlug, readItem(cwd, itemPath, env)),
469
- ),
475
+ ensureTaskDispatchable(artifact, newSlug, drafted),
470
476
  'promotedFrom',
471
477
  item,
472
478
  );
@@ -21,7 +21,9 @@
21
21
  * trigger surface — the forward seam the spec's Applied Answers q3 names.
22
22
  */
23
23
 
24
+ import {ACTION_PINS, pinnedUses} from './install-ci-action-pins.js';
24
25
  import type {ResolvedCIConfig} from './install-ci-core.js';
26
+ import {shouldDropCheckoutCredentials} from './install-ci-core.js';
25
27
 
26
28
  /**
27
29
  * The required-status-check CONTEXT install-ci sets via branch protection AND the
@@ -47,7 +49,18 @@ export const VERIFY_WORKFLOW_PATH = 'workflows/verify.yml';
47
49
  * so the GitHub check it produces matches the required-status `context` install-ci
48
50
  * configures on `main` — by construction, not by hand-typed agreement.
49
51
  */
50
- export function generateVerifyWorkflow(_config: ResolvedCIConfig): string {
52
+ export function generateVerifyWorkflow(config: ResolvedCIConfig): string {
53
+ // `persist-credentials: false` ONLY on a repo KNOWN to be public (see
54
+ // {@link shouldDropCheckoutCredentials}): this job has `contents: read`, so
55
+ // the persisted token can only ever read, and on a public repo reads need
56
+ // no token. Unknown / private / internal ⇒ today's checkout, unchanged.
57
+ const checkoutCredentials = shouldDropCheckoutCredentials(config)
58
+ ? `
59
+ # Public repository: every git read works without a token, and this
60
+ # job cannot push, so do not leave the token in .git/config for later
61
+ # steps (the project-setup hook's installs, the gate) to read.
62
+ persist-credentials: false`
63
+ : '';
51
64
  return `\
52
65
  # dorfl — the VERIFY workflow (Tier-1 GitHub ceiling; spec
53
66
  # land-time-reverify-and-parallel-merge-ceiling, task
@@ -100,9 +113,9 @@ jobs:
100
113
  ${VERIFY_CHECK_CONTEXT}:
101
114
  runs-on: ubuntu-latest
102
115
  steps:
103
- - uses: actions/checkout@v5
116
+ - uses: ${pinnedUses(ACTION_PINS.checkout)}
104
117
  with:
105
- fetch-depth: 0
118
+ fetch-depth: 0${checkoutCredentials}
106
119
  - uses: ./.github/actions/dorfl-setup
107
120
  - name: run the repo verify gate
108
121
  run: dorfl verify